diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 9faec847d..0306460a7 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,7 +1,13 @@ { - "ImportPath": "github.com/SkygearIO/skygear-server", - "GoVersion": "go1.5", + "ImportPath": "github.com/skygeario/skygear-server", + "GoVersion": "go1.6", + "GodepVersion": "v74", "Deps": [ + { + "ImportPath": "github.com/RobotsAndPencils/buford/push", + "Comment": "v0.10.0", + "Rev": "2d03c359eab35051daee2c71485309361639bed1" + }, { "ImportPath": "github.com/Sirupsen/logrus", "Comment": "v0.9.0-13-g74bde9e", @@ -106,10 +112,6 @@ "Comment": "v1-2-g67823cd", "Rev": "67823cd24dece1b04cced3a0a0b3ca2bc84d875e" }, - { - "ImportPath": "github.com/timehop/apns", - "Rev": "02b971829dfca0436e051a28d60f11ffec5e3516" - }, { "ImportPath": "github.com/twinj/uuid", "Rev": "70cac2bcd273ef6a371bb96cde363d28b68734c3" @@ -128,7 +130,19 @@ }, { "ImportPath": "golang.org/x/net/context", - "Rev": "db8e4de5b2d6653f66aea53094624468caad15d2" + "Rev": "b400c2eff1badec7022a8c8f5bea058b6315eed7" + }, + { + "ImportPath": "golang.org/x/net/http2", + "Rev": "b400c2eff1badec7022a8c8f5bea058b6315eed7" + }, + { + "ImportPath": "golang.org/x/net/http2/hpack", + "Rev": "b400c2eff1badec7022a8c8f5bea058b6315eed7" + }, + { + "ImportPath": "golang.org/x/net/lex/httplex", + "Rev": "b400c2eff1badec7022a8c8f5bea058b6315eed7" }, { "ImportPath": "golang.org/x/sys/unix", diff --git a/main.go b/main.go index 99aaaa6d0..4604ae689 100644 --- a/main.go +++ b/main.go @@ -355,7 +355,6 @@ func initAPNSPusher(config skyconfig.Configuration, connOpener func() (skydb.Con log.Fatalf("Failed to set up push sender: %v", err) } go apnsPushSender.Run() - go apnsPushSender.RunFeedback() return apnsPushSender } diff --git a/push/apns.go b/push/apns.go index ddec85006..85119ce34 100644 --- a/push/apns.go +++ b/push/apns.go @@ -15,14 +15,18 @@ package push import ( + "crypto/tls" + "crypto/x509" + "encoding/asn1" "encoding/json" "errors" "fmt" + "net/http" "time" + "github.com/RobotsAndPencils/buford/push" log "github.com/Sirupsen/logrus" "github.com/skygeario/skygear-server/skydb" - "github.com/timehop/apns" ) // GatewayType determine which kind of gateway should be used for APNS @@ -34,15 +38,9 @@ const ( Production = "production" ) -// private interface s.t. we can mock apns.Client in test -type apnsSender interface { - Send(n apns.Notification) error - FailedNotifs() chan apns.NotificationResult -} - -// private interface to mock apns.Feedback in test -type feedbackReceiver interface { - Receive() <-chan apns.FeedbackTuple +// private interface s.t. we can mock push.Service in test +type pushService interface { + Push(deviceToken string, headers *push.Headers, payload []byte) (string, error) } // APNSPusher pushes notification via apns @@ -50,198 +48,221 @@ type APNSPusher struct { // Function to obtain a skydb connection connOpener func() (skydb.Conn, error) - // we are directly coupling on apns as it seems redundant to duplicate - // all the payload and client logic and interfaces. - client apnsSender + conn skydb.Conn + service pushService + failed chan failedNotification + topic string +} + +type failedNotification struct { + deviceToken string + err push.Error +} + +// parseCertificateLeaf parse the provided TLS certificate for its +// leaf certificate. Returns an error if the leaf certificate cannot be found. +func parseCertificateLeaf(certificate *tls.Certificate) error { + if certificate.Leaf != nil { + return nil + } + + for _, cert := range certificate.Certificate { + x509Cert, err := x509.ParseCertificate(cert) + if err != nil { + return err + } + certificate.Leaf = x509Cert + return nil + } + return errors.New("push/apns: provided APNS certificate does not contain leaf") +} + +// findDefaultAPNSTopic returns the APNS topic in the TLS certificate. +// +// The Subject of leaf certificate should contains the UID, which we can +// use as the topic for APNS. The topic is usually the same as the +// application bundle // identifier. +// +// Returns the topic name, and an error if an error occuring finding the topic +// name. +func findDefaultAPNSTopic(certificate tls.Certificate) (string, error) { + if certificate.Leaf == nil { + err := parseCertificateLeaf(&certificate) + if err != nil { + return "", err + } + } + + // Loop over the subject names array to look for UID + uidObjectIdentifier := asn1.ObjectIdentifier([]int{0, 9, 2342, 19200300, 100, 1, 1}) + for _, attr := range certificate.Leaf.Subject.Names { + if uidObjectIdentifier.Equal(attr.Type) { + switch value := attr.Value.(type) { + case string: + return value, nil + } + break + } + } - feedback feedbackReceiver + return "", errors.New("push/apns: cannot find UID in APNS certificate subject name") } // NewAPNSPusher returns a new APNSPusher from content of certificate // and private key as string func NewAPNSPusher(connOpener func() (skydb.Conn, error), gwType GatewayType, cert string, key string) (*APNSPusher, error) { - var gateway, fbGateway string - switch gwType { - case Sandbox: - gateway = apns.SandboxGateway - fbGateway = apns.SandboxFeedbackGateway - case Production: - gateway = apns.ProductionGateway - fbGateway = apns.ProductionFeedbackGateway - default: - return nil, fmt.Errorf("unrecgonized GatewayType = %#v", gwType) + certificate, err := tls.X509KeyPair([]byte(cert), []byte(key)) + if err != nil { + return nil, err } - client, err := apns.NewClient(gateway, cert, key) + topic, err := findDefaultAPNSTopic(certificate) if err != nil { return nil, err } - fb, err := apns.NewFeedback(fbGateway, cert, key) + client, err := push.NewClient(certificate) if err != nil { return nil, err } + var service *push.Service + switch gwType { + case Sandbox: + service = push.NewService(client, push.Development) + case Production: + service = push.NewService(client, push.Production) + default: + return nil, fmt.Errorf("push/apns: unrecognized gateway type %s", gwType) + } + return &APNSPusher{ connOpener: connOpener, - client: &wrappedClient{&client}, - feedback: fb, + service: service, + topic: topic, }, nil } // Run listens to the notification error channel func (pusher *APNSPusher) Run() { - for result := range pusher.client.FailedNotifs() { - log.Errorf("Failed to send notification = %s: %v", result.Notif.ID, result.Err) - } -} - -// RunFeedback kicks start receiving from the Feedback Service. -// -// The checking behaviour is to: -// 1. Receive once on startup -// 2. Receive once at 00:00:00 everyday -func (pusher *APNSPusher) RunFeedback() { - pusher.recvFeedback() - - for { - now := time.Now() - year, month, day := now.Date() - nextDay := time.Date(year, month, day+1, 0, 0, 0, 0, time.UTC) - d := nextDay.Sub(now) - - log.Infof("apns/fb: next feedback scheduled after %v, at %v", d, nextDay) - - <-time.After(d) - - log.Infoln("apns/fb: going to query feedback service") - pusher.recvFeedback() - } -} - -func (pusher *APNSPusher) recvFeedback() { + pusher.failed = make(chan failedNotification) conn, err := pusher.connOpener() if err != nil { log.Errorf("apns/fb: failed to open skydb.Conn, abort feedback retrival: %v\n", err) return } - received := false - for fb := range pusher.feedback.Receive() { - log.Infof("apns/fb: got a feedback = %v", fb) + pusher.conn = conn - received = true + go func() { + pusher.checkFailedNotifications() + }() +} - // NOTE(limouren): it might be more elegant in the future to extend - // push.Sender as NotificationService and bridge over the differences - // between gcm and apns on handling unregistered devices (probably - // as an async channel) - if err := conn.DeleteDeviceByToken(fb.DeviceToken, fb.Timestamp); err != nil && err != skydb.ErrDeviceNotFound { - log.Errorf("apns/fb: failed to delete device token = %s: %v", fb.DeviceToken, err) - } - } +func (pusher *APNSPusher) Stop() { + close(pusher.failed) +} - if !received { - log.Infoln("apns/fb: no feedback received") +func (pusher *APNSPusher) checkFailedNotifications() { + for failedNote := range pusher.failed { + pusher.handleFailedNotification(failedNote) } } -func setPayloadAPS(apsMap map[string]interface{}, aps *apns.APS) { - for key, value := range apsMap { - switch key { - case "content-available": - switch value := value.(type) { - case int: - aps.ContentAvailable = value - case float64: - aps.ContentAvailable = int(value) - } - case "sound": - if sound, ok := value.(string); ok { - aps.Sound = sound - } - case "badge": - switch value := value.(type) { - case int: - aps.Badge.Set(uint(value)) - case float64: - aps.Badge.Set(uint(value)) - } - case "alert": - if body, ok := value.(string); ok { - aps.Alert.Body = body - } else if alertMap, ok := value.(map[string]interface{}); ok { - jsonbytes, err := json.Marshal(&alertMap) - if err != nil { - panic("Unable to convert alert to json.") - } - - err = json.Unmarshal(jsonbytes, &aps.Alert) - if err != nil { - panic("Unable to convert json back to Alert struct.") - } - } - } +func (pusher *APNSPusher) queueFailedNotification(deviceToken string, err push.Error) bool { + logger := log.WithFields(log.Fields{ + "deviceToken": deviceToken, + }) + failed := pusher.failed + if failed == nil { + logger.Warn("Unable to queue failed notification for error handling because the pusher is not running") + return false + } + failed <- failedNotification{ + deviceToken: deviceToken, + err: err, } + logger.Debug("Queued failed notification for error handling") + return true } -func setPayload(m map[string]interface{}, p *apns.Payload) { - if apsValue, ok := m["aps"]; ok { - if apsMap, ok := apsValue.(map[string]interface{}); ok { - setPayloadAPS(apsMap, &p.APS) - } else { - log.Errorf("Want aps.(type) be map[string]interface{}, got %T", apsValue) - } +func shouldUnregisterDevice(failedNote failedNotification) bool { + return failedNote.err.Status == http.StatusGone || failedNote.err.Reason.Error() == "BadDeviceToken" +} + +func (pusher *APNSPusher) handleFailedNotification(failedNote failedNotification) { + if shouldUnregisterDevice(failedNote) { + pusher.unregisterDevice(failedNote.deviceToken, failedNote.err.Timestamp) } +} - // set custom values - for key, value := range m { - // the "aps" key is not a custom key - if key == "aps" { - continue - } - if err := p.SetCustomValue(key, value); err != nil { - log.Errorf("Failed to set data[%v] = %v", key, value) +func (pusher *APNSPusher) unregisterDevice(deviceToken string, timestamp time.Time) { + logger := log.WithFields(log.Fields{ + "deviceToken": deviceToken, + }) + + defer func() { + if r := recover(); r != nil { + logger.Panicf("Panic occurred while unregistering device: %s", r) } + }() + + if err := pusher.conn.DeleteDevicesByToken(deviceToken, timestamp); err != nil && err != skydb.ErrDeviceNotFound { + logger.Errorf("apns/fb: failed to delete device token = %s: %v", deviceToken, err) + return } + + logger.Info("Unregistered device from skydb") } // Send sends a notification to the device identified by the // specified device func (pusher *APNSPusher) Send(m Mapper, device skydb.Device) error { + logger := log.WithFields(log.Fields{ + "deviceToken": device.Token, + "deviceID": device.ID, + "apnsTopic": pusher.topic, + }) + if m == nil { + logger.Warn("Cannot send push notification with nil data.") return nil } + apnsMap, ok := m.Map()["apns"].(map[string]interface{}) if !ok { return errors.New("push/apns: payload has no apns dictionary") } - payload := apns.NewPayload() - setPayload(apnsMap, payload) - - notification := apns.NewNotification() - notification.Payload = payload - notification.DeviceToken = device.Token - notification.Priority = apns.PriorityImmediate - - if err := pusher.client.Send(notification); err != nil { - log.Errorf("Failed to send APNS Notification: %v", err) + serializedPayload, err := json.Marshal(apnsMap) + if err != nil { return err } - return nil -} + headers := push.Headers{ + Topic: pusher.topic, + } -// wrapper of apns.Client which implement apnsSender -type wrappedClient struct { - ci *apns.Client -} + // push the notification: + apnsid, err := pusher.service.Push(device.Token, &headers, serializedPayload) + if err != nil { + if pushError, ok := err.(*push.Error); ok && pushError != nil { + // We recognize the error, and that error comes from APNS + logger.WithFields(log.Fields{ + "apnsErrorReason": pushError.Reason, + "apnsErrorStatus": pushError.Status, + "apnsErrorTimestamp": pushError.Timestamp, + }).Error("Failed to send push notification to APNS") + pusher.queueFailedNotification(device.Token, *pushError) + return err + } -func (c *wrappedClient) Send(n apns.Notification) error { - return c.ci.Send(n) -} + logger.Errorf("Failed to send push notification: %s", err) + return err + } -func (c *wrappedClient) FailedNotifs() chan apns.NotificationResult { - return c.ci.FailedNotifs + logger.WithFields(log.Fields{ + "apnsID": apnsid, + }).Info("Sent push notification to APNS") + return nil } diff --git a/push/apns_test.go b/push/apns_test.go index 7c94a862c..a2cbb877e 100644 --- a/push/apns_test.go +++ b/push/apns_test.go @@ -15,37 +15,43 @@ package push import ( - "encoding/json" + "crypto/tls" "errors" + "net/http" "testing" "time" + "github.com/RobotsAndPencils/buford/push" "github.com/skygeario/skygear-server/skydb" . "github.com/skygeario/skygear-server/skytest" . "github.com/smartystreets/goconvey/convey" - "github.com/timehop/apns" ) -type naiveClient struct { - failedNotifs chan apns.NotificationResult - sentNotifications []apns.Notification - returnerr error +type naiveServiceNotification struct { + DeviceToken string + Headers *push.Headers + Payload []byte } -func (c *naiveClient) Send(n apns.Notification) error { - c.sentNotifications = append(c.sentNotifications, n) - return c.returnerr +type naiveService struct { + Sent []naiveServiceNotification + Err *push.Error } -func (c *naiveClient) FailedNotifs() chan apns.NotificationResult { - return c.failedNotifs +func (s *naiveService) Push(deviceToken string, headers *push.Headers, payload []byte) (string, error) { + s.Sent = append(s.Sent, naiveServiceNotification{deviceToken, headers, payload}) + if s.Err != nil { + return "", s.Err + } + return "77BAF428-6FD8-42DB-8D1E-6F14A36C0863", nil } func TestAPNSSend(t *testing.T) { Convey("APNSPusher", t, func() { - client := naiveClient{} + service := naiveService{} pusher := APNSPusher{ - client: &client, + service: &service, + failed: make(chan failedNotification, 10), } device := skydb.Device{ Token: "deviceToken", @@ -56,6 +62,7 @@ func TestAPNSSend(t *testing.T) { Convey("pushes notification", func() { customMap := MapMapper{ + "apns": map[string]interface{}{ "aps": map[string]interface{}{ "content-available": 1, @@ -73,14 +80,13 @@ func TestAPNSSend(t *testing.T) { So(pusher.Send(customMap, device), ShouldBeNil) So(pusher.Send(customMap, secondDevice), ShouldBeNil) - So(len(client.sentNotifications), ShouldEqual, 2) - So(client.sentNotifications[0].DeviceToken, ShouldEqual, "deviceToken") - So(client.sentNotifications[1].DeviceToken, ShouldEqual, "deviceToken2") - - for i := range client.sentNotifications { - n := client.sentNotifications[i] - payloadJSON, _ := json.Marshal(&n.Payload) - So(payloadJSON, ShouldEqualJSON, `{ + So(len(service.Sent), ShouldEqual, 2) + So(service.Sent[0].DeviceToken, ShouldEqual, "deviceToken") + So(service.Sent[1].DeviceToken, ShouldEqual, "deviceToken2") + + for i := range service.Sent { + n := service.Sent[i] + So(string(n.Payload), ShouldEqualJSON, `{ "aps": { "content-available": 1, "sound": "sosumi.mp3", @@ -101,12 +107,33 @@ func TestAPNSSend(t *testing.T) { So(err, ShouldResemble, errors.New("push/apns: payload has no apns dictionary")) }) - Convey("returns error returned from Client.Send", func() { - client.returnerr = errors.New("apns_test: some error") + Convey("returns error returned from Service.Push (BadMessageId)", func() { + service.Err = &push.Error{ + Reason: errors.New("BadMessageId"), + Status: http.StatusBadRequest, + Timestamp: time.Time{}, + } + err := pusher.Send(MapMapper{ + "apns": map[string]interface{}{}, + }, device) + So(err, ShouldResemble, service.Err) + }) + + Convey("returns error returned from Service.Push (Unregistered)", func() { + pushError := push.Error{ + Reason: errors.New("Unregistered"), + Status: http.StatusGone, + Timestamp: time.Now(), + } + service.Err = &pushError err := pusher.Send(MapMapper{ "apns": map[string]interface{}{}, }, device) - So(err, ShouldResemble, errors.New("apns_test: some error")) + So(err, ShouldResemble, &pushError) + So(<-pusher.failed, ShouldResemble, failedNotification{ + deviceToken: device.Token, + err: pushError, + }) }) Convey("pushes with custom alert", func() { @@ -125,11 +152,10 @@ func TestAPNSSend(t *testing.T) { So(err, ShouldBeNil) - n := client.sentNotifications[0] + n := service.Sent[0] So(n.DeviceToken, ShouldEqual, "deviceToken") - payloadJSON, _ := json.Marshal(&n.Payload) - So(payloadJSON, ShouldEqualJSON, `{ + So(string(n.Payload), ShouldEqualJSON, `{ "aps": { "alert": { "body": "Acme message received from Johnny Appleseed", @@ -152,7 +178,7 @@ type mockConn struct { skydb.Conn } -func (c *mockConn) DeleteDeviceByToken(token string, t time.Time) error { +func (c *mockConn) DeleteDevicesByToken(token string, t time.Time) error { c.calls = append(c.calls, deleteCall{token, t}) return c.err } @@ -161,56 +187,119 @@ func (c *mockConn) Open() (skydb.Conn, error) { return c, nil } -type feedbackChannel chan apns.FeedbackTuple - -func (ch feedbackChannel) Receive() <-chan apns.FeedbackTuple { - return ch -} - func TestAPNSFeedback(t *testing.T) { Convey("APNSPusher", t, func() { conn := &mockConn{} - ch := make(chan apns.FeedbackTuple) pusher := APNSPusher{ connOpener: conn.Open, - feedback: feedbackChannel(ch), + conn: conn, } - Convey("receives no feedbacks", func() { - close(ch) - pusher.recvFeedback() - So(conn.calls, ShouldBeEmpty) + Convey("unregister device", func() { + pusher.unregisterDevice("devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)) + So(conn.calls, ShouldResemble, []deleteCall{ + {"devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)}, + }) }) - Convey("receives multiple feedbacks", func() { - go func() { - ch <- newFeedbackTuple("devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)) - ch <- newFeedbackTuple("devicetoken1", time.Date(2046, 1, 2, 15, 4, 5, 0, time.UTC)) - close(ch) - }() + Convey("unregister device with error", func() { + conn.err = errors.New("apns/test: unknown error") + pusher.unregisterDevice("devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)) + So(conn.calls, ShouldResemble, []deleteCall{ + {"devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)}, + }) + }) - pusher.recvFeedback() + Convey("handle unregistered notification", func() { + pusher.handleFailedNotification(failedNotification{"devicetoken0", push.Error{ + Reason: errors.New("Unregistered"), + Status: http.StatusGone, + Timestamp: time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC), + }}) So(conn.calls, ShouldResemble, []deleteCall{ {"devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)}, - {"devicetoken1", time.Date(2046, 1, 2, 15, 4, 5, 0, time.UTC)}, }) }) - Convey("handles erroneous device delete", func() { + Convey("check failed notifications", func() { + pusher.failed = make(chan failedNotification) go func() { - ch <- newFeedbackTuple("devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)) - close(ch) + pushError1 := push.Error{ + Reason: errors.New("Unregistered"), + Status: http.StatusGone, + Timestamp: time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC), + } + pusher.failed <- failedNotification{"devicetoken0", pushError1} + + pushError2 := push.Error{ + Reason: errors.New("Unregistered"), + Status: http.StatusGone, + Timestamp: time.Date(2046, 1, 2, 15, 4, 5, 0, time.UTC), + } + pusher.failed <- failedNotification{"devicetoken1", pushError2} + close(pusher.failed) }() - conn.err = errors.New("apns/test: unknown error") - pusher.recvFeedback() + pusher.checkFailedNotifications() So(conn.calls, ShouldResemble, []deleteCall{ {"devicetoken0", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)}, + {"devicetoken1", time.Date(2046, 1, 2, 15, 4, 5, 0, time.UTC)}, }) }) }) } -func newFeedbackTuple(token string, t time.Time) apns.FeedbackTuple { - return apns.FeedbackTuple{Timestamp: t, DeviceToken: token} +func TestAPNSCertificate(t *testing.T) { + APNSCert := []byte(`-----BEGIN CERTIFICATE----- +MIICvjCCAaYCCQDrgz2ANVkBfTANBgkqhkiG9w0BAQsFADAhMR8wHQYKCZImiZPy +LGQBAQwPY29tLmV4YW1wbGUuQXBwMB4XDTE2MDcwODA3NTAzN1oXDTE3MDcwODA3 +NTAzN1owITEfMB0GCgmSJomT8ixkAQEMD2NvbS5leGFtcGxlLkFwcDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKd+cBNZ9lyKb81WGbNU4Vh4Z5TJB0tI +V7m3Iohl0sd8zbUhL8ISXPpKnPvwo2DZScs6Y4hJsZxQenNm5ll4cFAgcNHbu0I6 +T1VzLSnxtpgvDOdxOBN5Nw1syKfzMUJw8o8RMtRt9cYVBwlKvOI92agFqZVYCIA3 +4T/531f/VejIFd8wzp8fLMS+A8dJ+Run9Z4r4KZu8VhtKUP8GAFZ0pt9PL4Rm4Rl +/J/FZi5EmCE9Ms1RZoLuwO/IKPuGIY5rRi1c0kbYL3+QPxlkJa9DGW/61mDEkPkx +l6MvrbBFQIDSRUVQ97a8RNk/5tBwsAnyyqYxx9i9wjudNCv5YQs/QL8CAwEAATAN +BgkqhkiG9w0BAQsFAAOCAQEAb1mm4+6B+8YFqagAQ18I8EzOIHqrceDHj+v3PAh7 +jD+orKmbFnq6kbzEj9AHOp+A5EjSLIBIarXFJIsbRXenYwDLF+0dwFkXzzhLSsAO +kvsTQPFaQC/h3mV8stx2SLxTDpWMPaaNCOlPkTmEtqXA3fes/1hF6TYalYO6kHe8 +47iuzxKNjgfjjYeK3o4ccFS0+29WVoU5t+wuZ0Ha27PPNOFHLvn9TI9A5L+8ujgr +oyxSZaLz1oPX7aCcC847s+a73+K4V4QwdvKhxEN2McdZqv1h1Ha5zptt4kniBv6X +OFCnXiurw3uY37eBckl/JR++IkUekyIq1EJ0vfWyW/mhPQ== +-----END CERTIFICATE-----`) + APNSKey := []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAp35wE1n2XIpvzVYZs1ThWHhnlMkHS0hXubciiGXSx3zNtSEv +whJc+kqc+/CjYNlJyzpjiEmxnFB6c2bmWXhwUCBw0du7QjpPVXMtKfG2mC8M53E4 +E3k3DWzIp/MxQnDyjxEy1G31xhUHCUq84j3ZqAWplVgIgDfhP/nfV/9V6MgV3zDO +nx8sxL4Dx0n5G6f1nivgpm7xWG0pQ/wYAVnSm308vhGbhGX8n8VmLkSYIT0yzVFm +gu7A78go+4YhjmtGLVzSRtgvf5A/GWQlr0MZb/rWYMSQ+TGXoy+tsEVAgNJFRVD3 +trxE2T/m0HCwCfLKpjHH2L3CO500K/lhCz9AvwIDAQABAoIBAQCgZYyee4BZjpkS +YmmqOpaySlunN/wsM9MOnjoLtLbtIq87zdQWXc98QQeknQVYMb1hSUEXurrDnq4k +5V2iQJwNn4Nq9KmW+pAOnIWbrUXW5vfMi7fPrjzyNkLR0ypRHiiqqSWsGMFMN8bN +Ny0621Acf4+u3OcHInwq7/baJkL271g1m0hX/7TJ/nv+SlO00IkB6tm8iU0LWant +4fSvhV2ULxa0fF5XXx74jqDFF+NzJ45XMUDbbe72RKpydMsdprqp4BgaV7fbtHIx +xjG/9z6KqM3v0bMkJFV9BnmWzNe7vrI96dizVR3w3Yygul7sNC2iZfe17ulaXXKA +n3X+PBTBAoGBANkwAMW+dQw9x/dDIQgfkg27I7qgANdYyri/lJ6+GvMdcCd4mzQ+ +UnJzhPNJyPZSUZeHOthShbHvBcoYNi4AwEcor2bXJI3+jW3/Wqbpq87+9x3Hkk69 +tkKvESKy8IABOTntFD3/VGFjYiLXVpg0huvkRSlSv70gMusmTMxisvWNAoGBAMVt +Bx9IVkZ4uFl565WmigUuO8ICgHeRovCqeF3g0YBlS/x9Jznd9QkMYT0hy9cE4b7Z +GRm89mEOUclWgdsibkyEdD1qOF6xF+fi4xKaes8Vc3vuuhg+UqpYvbIxKFAT81A2 +adyL6npDJDqQ0DcRjMng8V/77ktLOe/g9HFLX957AoGAOIlKai9eAMXEXBVZb+fn ++TMR5e7oySYP/2+/nGMYWNj87Ql0PXFLvQddQIegjJ55JtzI8K7qppr2AtmyoN8J +Lnzky/yNQ3lUD6I9Ut3ZH5U3dsUQzPaNj2ZLK6ExAeFPqEiS0GC68m8QiMlNfWmP +BbDyYANubikHmDbsHvhCZbECgYA0hx62/wsdcu8xt1OsHIRqfnOd2gaOSax9tg2S +hMeZDtqZ0j7GkbypbKbOmhhfHEhn++FGzNUM2798/0xLnqyUJUW8NW/MGfhPVTmv +cHSudnmkhs7ytlpOQpAuQhAExlodhGzEJmH7p7OS9YbAsCWybOwr6p7rX5eJsGO5 +ZSGb0wKBgQCvYtVVUGPBbmp94yoRgQjPj+iZtSjmVA9LaNoX+g4UWxKTU3y+r9kg +SZVlxzxtvFvO+LcxmP6wBSX30HmhtIFLxmOyySl6BjfbtE0uFnIxxrKhb2L0gqoN +g723fJntDb71I1IS31Vd2wqqpVB4kDp8OiPnPp8ats/cNUFk77Jhxw== +-----END RSA PRIVATE KEY-----`) + + Convey("find apns topic", t, func() { + certificate, err := tls.X509KeyPair(APNSCert, APNSKey) + So(err, ShouldBeNil) + topic, err := findDefaultAPNSTopic(certificate) + So(err, ShouldBeNil) + So(topic, ShouldEqual, "com.example.App") + }) } diff --git a/skydb/conn.go b/skydb/conn.go index bf3dcb9b7..40ab3b4dc 100644 --- a/skydb/conn.go +++ b/skydb/conn.go @@ -31,7 +31,7 @@ var ErrUserNotFound = errors.New("skydb: UserInfo ID not found") var ErrRoleUpdatesFailed = errors.New("skydb: Update of user roles failed") // ErrDeviceNotFound is returned by Conn.GetDevice, Conn.DeleteDevice, -// Conn.DeleteDeviceByToken and Conn.DeleteEmptyDevicesByTime, if the desired Device +// Conn.DeleteDevicesByToken and Conn.DeleteEmptyDevicesByTime, if the desired Device // cannot be found in the current container var ErrDeviceNotFound = errors.New("skydb: Specific device not found") @@ -39,7 +39,7 @@ var ErrDeviceNotFound = errors.New("skydb: Specific device not found") // operation modifies the database and the database is readonly. var ErrDatabaseIsReadOnly = errors.New("skydb: database is read only") -// ZeroTime represent a zero time.Time. It is used in DeleteDeviceByToken and +// ZeroTime represent a zero time.Time. It is used in DeleteDevicesByToken and // DeleteEmptyDevicesByTime to signify a Delete without time constraint. var ZeroTime = time.Time{} @@ -134,11 +134,11 @@ type Conn interface { SaveDevice(device *Device) error DeleteDevice(id string) error - // DeleteDeviceByToken deletes device where its Token == token and + // DeleteDevicesByToken deletes device where its Token == token and // LastRegisteredAt < t. If t == ZeroTime, LastRegisteredAt is not considered. // // If such device does not exist, ErrDeviceNotFound is returned. - DeleteDeviceByToken(token string, t time.Time) error + DeleteDevicesByToken(token string, t time.Time) error // DeleteEmptyDevicesByTime deletes device where Token is empty and // LastRegisteredAt < t. If t == ZeroTime, LastRegisteredAt is not considered. diff --git a/skydb/mock_skydb/mock.go b/skydb/mock_skydb/mock.go index 73a25d720..672dc984f 100644 --- a/skydb/mock_skydb/mock.go +++ b/skydb/mock_skydb/mock.go @@ -70,14 +70,14 @@ func (_mr *_MockConnRecorder) DeleteDevice(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteDevice", arg0) } -func (_m *MockConn) DeleteDeviceByToken(_param0 string, _param1 time.Time) error { - ret := _m.ctrl.Call(_m, "DeleteDeviceByToken", _param0, _param1) +func (_m *MockConn) DeleteDevicesByToken(_param0 string, _param1 time.Time) error { + ret := _m.ctrl.Call(_m, "DeleteDevicesByToken", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } -func (_mr *_MockConnRecorder) DeleteDeviceByToken(arg0, arg1 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteDeviceByToken", arg0, arg1) +func (_mr *_MockConnRecorder) DeleteDevicesByToken(arg0, arg1 interface{}) *gomock.Call { + return _mr.mock.ctrl.RecordCall(_mr.mock, "DeleteDevicesByToken", arg0, arg1) } func (_m *MockConn) DeleteEmptyDevicesByTime(_param0 time.Time) error { diff --git a/skydb/pq/device.go b/skydb/pq/device.go index 826417787..8c4b2bde9 100644 --- a/skydb/pq/device.go +++ b/skydb/pq/device.go @@ -125,7 +125,7 @@ func (c *conn) DeleteDevice(id string) error { return nil } -func (c *conn) DeleteDeviceByToken(token string, t time.Time) error { +func (c *conn) DeleteDevicesByToken(token string, t time.Time) error { builder := psql.Delete(c.tableName("_device")). Where("token = ?", token) if t != skydb.ZeroTime { @@ -143,8 +143,6 @@ func (c *conn) DeleteDeviceByToken(token string, t time.Time) error { } if rowsAffected == 0 { return skydb.ErrDeviceNotFound - } else if rowsAffected > 1 { - panic(fmt.Errorf("want 1 rows updated, got %v", rowsAffected)) } return nil diff --git a/skydb/pq/device_test.go b/skydb/pq/device_test.go index 2ae8549d9..5b2938cbc 100644 --- a/skydb/pq/device_test.go +++ b/skydb/pq/device_test.go @@ -193,7 +193,7 @@ func TestDevice(t *testing.T) { } So(c.SaveDevice(&device), ShouldBeNil) - err := c.DeleteDeviceByToken("devicetoken", skydb.ZeroTime) + err := c.DeleteDevicesByToken("devicetoken", skydb.ZeroTime) So(err, ShouldBeNil) var count int @@ -212,7 +212,7 @@ func TestDevice(t *testing.T) { } So(c.SaveDevice(&device), ShouldBeNil) - err := c.DeleteDeviceByToken("devicetoken", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)) + err := c.DeleteDevicesByToken("devicetoken", time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)) So(err, ShouldEqual, skydb.ErrDeviceNotFound) }) diff --git a/skydb/skydbtest/skydbtest.go b/skydb/skydbtest/skydbtest.go index c9d48c4f9..9bc46aea3 100644 --- a/skydb/skydbtest/skydbtest.go +++ b/skydb/skydbtest/skydbtest.go @@ -216,8 +216,8 @@ func (conn *MapConn) DeleteDevice(id string) error { panic("not implemented") } -// DeleteDeviceByToken is not implemented. -func (conn *MapConn) DeleteDeviceByToken(token string, t time.Time) error { +// DeleteDevicesByToken is not implemented. +func (conn *MapConn) DeleteDevicesByToken(token string, t time.Time) error { panic("not implemented") }