Merge lp:~pedronis/ubuntu-push/unicast-endp into lp:ubuntu-push/automatic

Proposed by Samuele Pedroni
Status: Merged
Approved by: Samuele Pedroni
Approved revision: 145
Merged at revision: 145
Proposed branch: lp:~pedronis/ubuntu-push/unicast-endp
Merge into: lp:ubuntu-push/automatic
Prerequisite: lp:~pedronis/ubuntu-push/unicast-broker
Diff against target: 527 lines (+311/-34)
4 files modified
Makefile (+1/-0)
dependencies.tsv (+1/-0)
server/api/handlers.go (+103/-21)
server/api/handlers_test.go (+206/-13)
To merge this branch: bzr merge lp:~pedronis/ubuntu-push/unicast-endp
Reviewer Review Type Date Requested Status
John Lenton (community) Approve
Review via email: mp+218045@code.launchpad.net

Commit message

/notify api endpoint for unicast, takes explicit user id,device id pair for now

Description of the change

/notify api endpoint for unicast, takes explicit user id,device id pair for now

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) :
review: Approve
145. By Samuele Pedroni

Merged unicast-broker into unicast-endp.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2014-05-01 10:24:23 +0000
3+++ Makefile 2014-05-02 15:13:47 +0000
4@@ -11,6 +11,7 @@
5 GODEPS += launchpad.net/go-dbus/v1
6 GODEPS += launchpad.net/go-xdg/v0
7 GODEPS += code.google.com/p/gosqlite/sqlite3
8+GODEPS += launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid/uuid
9
10 TOTEST = $(shell env GOPATH=$(GOPATH) go list $(PROJECT)/...|grep -v acceptance|grep -v http13client )
11
12
13=== modified file 'dependencies.tsv'
14--- dependencies.tsv 2014-04-30 16:44:16 +0000
15+++ dependencies.tsv 2014-05-02 15:13:47 +0000
16@@ -2,3 +2,4 @@
17 launchpad.net/go-dbus/v1 bzr james@jamesh.id.au-20140206110213-pbzcr6ucaz3rqmnw 125
18 launchpad.net/go-xdg/v0 bzr john.lenton@canonical.com-20140208094800-gubd5md7cro3mtxa 10
19 launchpad.net/gocheck bzr gustavo@niemeyer.net-20140127131816-zshobk1qqme626xw 86
20+launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid bzr samuele.pedroni@canonical.com-20140130122455-pm9h8etl4owp90lg 1
21
22=== modified file 'server/api/handlers.go'
23--- server/api/handlers.go 2014-02-20 17:09:03 +0000
24+++ server/api/handlers.go 2014-05-02 15:13:47 +0000
25@@ -19,12 +19,15 @@
26 package api
27
28 import (
29+ "encoding/base64"
30 "encoding/json"
31 "fmt"
32 "io"
33 "net/http"
34 "time"
35
36+ "launchpad.net/~ubuntu-push-hackers/ubuntu-push/go-uuid/uuid"
37+
38 "launchpad.net/ubuntu-push/logger"
39 "launchpad.net/ubuntu-push/server/broker"
40 "launchpad.net/ubuntu-push/server/store"
41@@ -93,6 +96,11 @@
42 ioError,
43 "Could not read request body",
44 }
45+ ErrMissingIdField = &APIError{
46+ http.StatusBadRequest,
47+ invalidRequest,
48+ "Missing id field",
49+ }
50 ErrMissingData = &APIError{
51 http.StatusBadRequest,
52 invalidRequest,
53@@ -130,10 +138,17 @@
54 }
55 )
56
57-type Message struct {
58- Registration string `json:"registration"`
59- CoalesceTag string `json:"coalesce_tag"`
60- Data json.RawMessage `json:"data"`
61+type castCommon struct {
62+}
63+
64+type Unicast struct {
65+ UserId string `json:"userid"`
66+ DeviceId string `json:"deviceid"`
67+ AppId string `json:"appid"`
68+ //Registration string `json:"registration"`
69+ //CoalesceTag string `json:"coalesce_tag"`
70+ ExpireOn string `json:"expire_on"`
71+ Data json.RawMessage `json:"data"`
72 }
73
74 // Broadcast request JSON object.
75@@ -198,11 +213,11 @@
76
77 var zeroTime = time.Time{}
78
79-func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {
80- if len(bcast.Data) == 0 {
81+func checkCastCommon(data json.RawMessage, expireOn string) (time.Time, *APIError) {
82+ if len(data) == 0 {
83 return zeroTime, ErrMissingData
84 }
85- expire, err := time.Parse(time.RFC3339, bcast.ExpireOn)
86+ expire, err := time.Parse(time.RFC3339, expireOn)
87 if err != nil {
88 return zeroTime, ErrInvalidExpiration
89 }
90@@ -212,6 +227,10 @@
91 return expire, nil
92 }
93
94+func checkBroadcast(bcast *Broadcast) (time.Time, *APIError) {
95+ return checkCastCommon(bcast.Data, bcast.ExpireOn)
96+}
97+
98 type StoreForRequest func(w http.ResponseWriter, request *http.Request) (store.PendingStore, error)
99
100 // context holds the interfaces to delegate to serving requests
101@@ -234,6 +253,20 @@
102 return sto, nil
103 }
104
105+func (ctx *context) prepare(w http.ResponseWriter, request *http.Request, reqObj interface{}) (store.PendingStore, *APIError) {
106+ body, apiErr := ReadBody(request, MaxRequestBodyBytes)
107+ if apiErr != nil {
108+ return nil, apiErr
109+ }
110+
111+ err := json.Unmarshal(body, reqObj)
112+ if err != nil {
113+ return nil, ErrMalformedJSONObject
114+ }
115+
116+ return ctx.getStore(w, request)
117+}
118+
119 type BroadcastHandler struct {
120 *context
121 }
122@@ -270,23 +303,13 @@
123 }
124 }()
125
126- body, apiErr := ReadBody(request, MaxRequestBodyBytes)
127- if apiErr != nil {
128- return
129- }
130-
131- sto, apiErr := h.getStore(writer, request)
132- if apiErr != nil {
133- return
134- }
135- defer sto.Close()
136-
137 broadcast := &Broadcast{}
138- err := json.Unmarshal(body, broadcast)
139- if err != nil {
140- apiErr = ErrMalformedJSONObject
141+
142+ sto, apiErr := h.prepare(writer, request, broadcast)
143+ if apiErr != nil {
144 return
145 }
146+ defer sto.Close()
147
148 apiErr = h.doBroadcast(sto, broadcast)
149 if apiErr != nil {
150@@ -297,6 +320,64 @@
151 fmt.Fprintf(writer, `{"ok":true}`)
152 }
153
154+type UnicastHandler struct {
155+ *context
156+}
157+
158+func checkUnicast(ucast *Unicast) (time.Time, *APIError) {
159+ if ucast.UserId == "" || ucast.DeviceId == "" || ucast.AppId == "" {
160+ return zeroTime, ErrMissingIdField
161+ }
162+ return checkCastCommon(ucast.Data, ucast.ExpireOn)
163+}
164+
165+// use a base64 encoded TimeUUID
166+var generateMsgId = func() string {
167+ return base64.StdEncoding.EncodeToString(uuid.NewUUID())
168+}
169+
170+func (h *UnicastHandler) doUnicast(sto store.PendingStore, ucast *Unicast) *APIError {
171+ expire, apiErr := checkUnicast(ucast)
172+ if apiErr != nil {
173+ return apiErr
174+ }
175+ chanId := store.UnicastInternalChannelId(ucast.UserId, ucast.DeviceId)
176+ msgId := generateMsgId()
177+ err := sto.AppendToUnicastChannel(chanId, ucast.AppId, ucast.Data, msgId, expire)
178+ if err != nil {
179+ h.logger.Errorf("could not store notification: %v", err)
180+ return ErrCouldNotStoreNotification
181+ }
182+
183+ h.broker.Unicast(chanId)
184+ return nil
185+}
186+
187+func (h *UnicastHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
188+ var apiErr *APIError
189+ defer func() {
190+ if apiErr != nil {
191+ RespondError(writer, apiErr)
192+ }
193+ }()
194+
195+ unicast := &Unicast{}
196+
197+ sto, apiErr := h.prepare(writer, request, unicast)
198+ if apiErr != nil {
199+ return
200+ }
201+ defer sto.Close()
202+
203+ apiErr = h.doUnicast(sto, unicast)
204+ if apiErr != nil {
205+ return
206+ }
207+
208+ writer.Header().Set("Content-Type", "application/json")
209+ fmt.Fprintf(writer, `{"ok":true}`)
210+}
211+
212 // MakeHandlersMux makes a handler that dispatches for the various API endpoints.
213 func MakeHandlersMux(storeForRequest StoreForRequest, broker broker.BrokerSending, logger logger.Logger) *http.ServeMux {
214 ctx := &context{
215@@ -306,5 +387,6 @@
216 }
217 mux := http.NewServeMux()
218 mux.Handle("/broadcast", &BroadcastHandler{context: ctx})
219+ mux.Handle("/notify", &UnicastHandler{context: ctx})
220 return mux
221 }
222
223=== modified file 'server/api/handlers_test.go'
224--- server/api/handlers_test.go 2014-05-02 15:13:47 +0000
225+++ server/api/handlers_test.go 2014-05-02 15:13:47 +0000
226@@ -18,6 +18,7 @@
227
228 import (
229 "bytes"
230+ "encoding/base64"
231 "encoding/json"
232 "errors"
233 "fmt"
234@@ -32,7 +33,7 @@
235
236 "launchpad.net/ubuntu-push/protocol"
237 "launchpad.net/ubuntu-push/server/store"
238- helpers "launchpad.net/ubuntu-push/testing"
239+ help "launchpad.net/ubuntu-push/testing"
240 )
241
242 func TestHandlers(t *testing.T) { TestingT(t) }
243@@ -42,14 +43,14 @@
244 json string
245 client *http.Client
246 c *C
247- testlog *helpers.TestLogger
248+ testlog *help.TestLogger
249 }
250
251 var _ = Suite(&handlersSuite{})
252
253 func (s *handlersSuite) SetUpTest(c *C) {
254 s.client = &http.Client{}
255- s.testlog = helpers.NewTestLogger(c, "error")
256+ s.testlog = help.NewTestLogger(c, "error")
257 }
258
259 func (s *handlersSuite) TestAPIError(c *C) {
260@@ -99,7 +100,7 @@
261
262 var future = time.Now().Add(4 * time.Hour).Format(time.RFC3339)
263
264-func (s *handlersSuite) TestCheckBroadcast(c *C) {
265+func (s *handlersSuite) TestCheckCastBroadcastAndCommon(c *C) {
266 payload := json.RawMessage(`{"foo":"bar"}`)
267 broadcast := &Broadcast{
268 Channel: "system",
269@@ -135,11 +136,11 @@
270 }
271
272 type checkBrokerSending struct {
273- store store.PendingStore
274- chanId store.InternalChannelId
275- err error
276- top int64
277- payloads []json.RawMessage
278+ store store.PendingStore
279+ chanId store.InternalChannelId
280+ err error
281+ top int64
282+ notifications []protocol.Notification
283 }
284
285 func (cbsend *checkBrokerSending) Broadcast(chanId store.InternalChannelId) {
286@@ -147,11 +148,15 @@
287 cbsend.err = err
288 cbsend.chanId = chanId
289 cbsend.top = top
290- cbsend.payloads = protocol.ExtractPayloads(notifications)
291+ cbsend.notifications = notifications
292 }
293
294 func (cbsend *checkBrokerSending) Unicast(chanIds ...store.InternalChannelId) {
295- // xxx later
296+ // for now
297+ if len(chanIds) != 1 {
298+ panic("not expecting many chan ids for now")
299+ }
300+ cbsend.Broadcast(chanIds[0])
301 }
302
303 func (s *handlersSuite) TestDoBroadcast(c *C) {
304@@ -168,7 +173,7 @@
305 c.Check(bsend.err, IsNil)
306 c.Check(bsend.chanId, Equals, store.SystemInternalChannelId)
307 c.Check(bsend.top, Equals, int64(1))
308- c.Check(bsend.payloads, DeepEquals, []json.RawMessage{payload})
309+ c.Check(bsend.notifications, DeepEquals, help.Ns(payload))
310 }
311
312 func (s *handlersSuite) TestDoBroadcastUnknownChannel(c *C) {
313@@ -197,6 +202,11 @@
314 return isto.intercept("AppendToChannel", err)
315 }
316
317+func (isto *interceptInMemoryPendingStore) AppendToUnicastChannel(chanId store.InternalChannelId, appId string, payload json.RawMessage, msgId string, expiration time.Time) error {
318+ err := isto.InMemoryPendingStore.AppendToUnicastChannel(chanId, appId, payload, msgId, expiration)
319+ return isto.intercept("AppendToUnicastChannel", err)
320+}
321+
322 func (s *handlersSuite) TestDoBroadcastUnknownError(c *C) {
323 sto := &interceptInMemoryPendingStore{
324 store.NewInMemoryPendingStore(),
325@@ -234,6 +244,115 @@
326 c.Check(s.testlog.Captured(), Equals, "ERROR could not store notification: fail\n")
327 }
328
329+func (s *handlersSuite) TestCheckUnicast(c *C) {
330+ payload := json.RawMessage(`{"foo":"bar"}`)
331+ unicast := func() *Unicast {
332+ return &Unicast{
333+ UserId: "user1",
334+ DeviceId: "DEV1",
335+ AppId: "app1",
336+ ExpireOn: future,
337+ Data: payload,
338+ }
339+ }
340+ u := unicast()
341+ expire, apiErr := checkUnicast(u)
342+ c.Assert(apiErr, IsNil)
343+ c.Check(expire.Format(time.RFC3339), Equals, future)
344+
345+ u = unicast()
346+ u.UserId = ""
347+ expire, apiErr = checkUnicast(u)
348+ c.Check(apiErr, Equals, ErrMissingIdField)
349+
350+ u = unicast()
351+ u.AppId = ""
352+ expire, apiErr = checkUnicast(u)
353+ c.Check(apiErr, Equals, ErrMissingIdField)
354+
355+ u = unicast()
356+ u.DeviceId = ""
357+ expire, apiErr = checkUnicast(u)
358+ c.Check(apiErr, Equals, ErrMissingIdField)
359+
360+ u = unicast()
361+ u.Data = json.RawMessage(nil)
362+ expire, apiErr = checkUnicast(u)
363+ c.Check(apiErr, Equals, ErrMissingData)
364+}
365+
366+func (s *handlersSuite) TestGenerateMsgId(c *C) {
367+ msgId := generateMsgId()
368+ decoded, err := base64.StdEncoding.DecodeString(msgId)
369+ c.Assert(err, IsNil)
370+ c.Check(decoded, HasLen, 16)
371+}
372+
373+func (s *handlersSuite) TestDoUnicast(c *C) {
374+ prevGenMsgId := generateMsgId
375+ defer func() {
376+ generateMsgId = prevGenMsgId
377+ }()
378+ generateMsgId = func() string {
379+ return "MSG-ID"
380+ }
381+ sto := store.NewInMemoryPendingStore()
382+ bsend := &checkBrokerSending{store: sto}
383+ bh := &UnicastHandler{&context{nil, bsend, nil}}
384+ payload := json.RawMessage(`{"a": 1}`)
385+ apiErr := bh.doUnicast(sto, &Unicast{
386+ UserId: "user1",
387+ DeviceId: "DEV1",
388+ AppId: "app1",
389+ ExpireOn: future,
390+ Data: payload,
391+ })
392+ c.Check(apiErr, IsNil)
393+ c.Check(bsend.err, IsNil)
394+ c.Check(bsend.chanId, Equals, store.UnicastInternalChannelId("user1", "DEV1"))
395+ c.Check(bsend.top, Equals, int64(0))
396+ c.Check(bsend.notifications, DeepEquals, []protocol.Notification{
397+ protocol.Notification{
398+ AppId: "app1",
399+ MsgId: "MSG-ID",
400+ Payload: payload,
401+ },
402+ })
403+}
404+
405+func (s *handlersSuite) TestDoUnicastMissingIdField(c *C) {
406+ sto := store.NewInMemoryPendingStore()
407+ bh := &UnicastHandler{}
408+ apiErr := bh.doUnicast(sto, &Unicast{
409+ ExpireOn: future,
410+ Data: json.RawMessage(`{"a": 1}`),
411+ })
412+ c.Check(apiErr, Equals, ErrMissingIdField)
413+}
414+
415+func (s *handlersSuite) TestDoUnicastCouldNotStoreNotification(c *C) {
416+ sto := &interceptInMemoryPendingStore{
417+ store.NewInMemoryPendingStore(),
418+ func(meth string, err error) error {
419+ if meth == "AppendToUnicastChannel" {
420+ return errors.New("fail")
421+ }
422+ return err
423+ },
424+ }
425+ ctx := &context{logger: s.testlog}
426+ bh := &UnicastHandler{ctx}
427+ apiErr := bh.doUnicast(sto, &Unicast{
428+ UserId: "user1",
429+ DeviceId: "DEV1",
430+ AppId: "app1",
431+ ExpireOn: future,
432+ Data: json.RawMessage(`{"a": 1}`),
433+ })
434+ c.Check(apiErr, Equals, ErrCouldNotStoreNotification)
435+ c.Check(s.testlog.Captured(), Equals, "ERROR could not store notification: fail\n")
436+}
437+
438 func newPostRequest(path string, message interface{}, server *httptest.Server) *http.Request {
439 packedMessage, err := json.Marshal(message)
440 if err != nil {
441@@ -274,7 +393,11 @@
442 }
443
444 func (bsend testBrokerSending) Unicast(chanIds ...store.InternalChannelId) {
445- // xxx later
446+ // for now
447+ if len(chanIds) != 1 {
448+ panic("not expecting many chan ids for now")
449+ }
450+ bsend.chanId <- chanIds[0]
451 }
452
453 func (s *handlersSuite) TestRespondsToBasicSystemBroadcast(c *C) {
454@@ -489,3 +612,73 @@
455
456 checkError(c, response, ErrWrongRequestMethod)
457 }
458+
459+func (s *handlersSuite) TestRespondsUnicast(c *C) {
460+ sto := store.NewInMemoryPendingStore()
461+ stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
462+ return sto, nil
463+ }
464+ bsend := testBrokerSending{make(chan store.InternalChannelId, 1)}
465+ testServer := httptest.NewServer(MakeHandlersMux(stoForReq, bsend, nil))
466+ defer testServer.Close()
467+
468+ payload := json.RawMessage(`{"foo":"bar"}`)
469+
470+ request := newPostRequest("/notify", &Unicast{
471+ UserId: "user2",
472+ DeviceId: "dev3",
473+ AppId: "app2",
474+ ExpireOn: future,
475+ Data: payload,
476+ }, testServer)
477+
478+ response, err := s.client.Do(request)
479+ c.Assert(err, IsNil)
480+
481+ c.Check(response.StatusCode, Equals, http.StatusOK)
482+ c.Check(response.Header.Get("Content-Type"), Equals, "application/json")
483+ body, err := getResponseBody(response)
484+ c.Assert(err, IsNil)
485+ c.Check(string(body), Matches, ".*ok.*")
486+
487+ chanId := store.UnicastInternalChannelId("user2", "dev3")
488+ c.Check(<-bsend.chanId, Equals, chanId)
489+ top, notifications, err := sto.GetChannelSnapshot(chanId)
490+ c.Assert(err, IsNil)
491+ c.Check(top, Equals, int64(0))
492+ c.Check(notifications, HasLen, 1)
493+}
494+
495+func (s *handlersSuite) TestCannotUnicastTooBigMessages(c *C) {
496+ testServer := httptest.NewServer(&UnicastHandler{})
497+ defer testServer.Close()
498+
499+ bigString := strings.Repeat("a", MaxRequestBodyBytes)
500+ dataString := fmt.Sprintf(`"%v"`, bigString)
501+
502+ request := newPostRequest("/", &Unicast{
503+ ExpireOn: future,
504+ Data: json.RawMessage([]byte(dataString)),
505+ }, testServer)
506+
507+ response, err := s.client.Do(request)
508+ c.Assert(err, IsNil)
509+ checkError(c, response, ErrRequestBodyTooLarge)
510+}
511+
512+func (s *handlersSuite) TestCannotUnicastWithMissingFields(c *C) {
513+ stoForReq := func(http.ResponseWriter, *http.Request) (store.PendingStore, error) {
514+ return store.NewInMemoryPendingStore(), nil
515+ }
516+ ctx := &context{stoForReq, nil, nil}
517+ testServer := httptest.NewServer(&UnicastHandler{ctx})
518+ defer testServer.Close()
519+
520+ request := newPostRequest("/", &Unicast{
521+ Data: json.RawMessage(`{"foo":"bar"}`),
522+ }, testServer)
523+
524+ response, err := s.client.Do(request)
525+ c.Assert(err, IsNil)
526+ checkError(c, response, ErrMissingIdField)
527+}

Subscribers

People subscribed via source and target branches