ducky/devices

Paddy 2015-12-14 Parent:1ae5bae472c1

15:c24a6c5fcd8c Go to Latest

ducky/devices/storer_test.go

Begin implementation on apiv1. Begin implementing the apiv1 package, which will define the first iteration of our API endpoints and logic. Each API package should be self-contained and able to run without depending on each other. Think of them as interfaces into manipulating the business logic defined in the devices package. The point is to have total control over backwards compatibility, as long as our business logic doesn't change. If that happens, we're in a bad place, but not as bad as it could be. This required us to pull in all our API tools; the api package, its dependencies, the scopeTypes package (so we can define scopes for our API), the trout router, etc. We also updated uuid to the latest, which now includes a license. Hooray? The new apiv1 package consists of a few things: * The devices.go file defines the types the API will use to communicate, along with some helpers to convert from API types to devices types. There's also a stub for validating the device creation requests, which I haven't implemented yet because I'm a pretty bad person. * endpoints.go just contains a helper function that builds our routes and assigns handlers to them, giving us an http.Handler in the returns that we can listen with. * handlers.go defines our HTTP handlers, which will read requests and write responses, after doing the appropriate validation and executing the appropriating business logic. Right now, we only have a handler for creating devices, and it doesn't actually do any validation. Also, we have some user-correctable errors being returned as 500s right now, which is Bad. Fortunately, they're all marked with BUG, so I can at least come back to them. * response.go defines the Response type that will be used for returning information after a request is executed. It may eventually get some helpers, but for now it's pretty basic. * scopes.go defines the Scopes that we're going to be using in the package to control access. It should probably (eventually) include a helper to register the Scopes, or we should have a collector service that pulls in all the packages, finds all their Scopes, and registers them. I haven't decided how I want to manage Scope registration just yet. We exported the getStorer function (now GetStorer) so other packages can use it. I'm not sure how I feel about this just yet. We also had to create a WithStorer helper method that embeds the Storer into a context.Context, so we can bootstrap in devicesd. We erroneously had Created in the DeviceChange struct, but there's no reason the Created property of a Device should ever change, so it was removed from the logic, from the struct, and from the tests. Our CreateMany helper was erroneously creating the un-modified Devices that were passed in, instead of the Devices that had sensible defaults filled. We created a _very minimal_ (e.g., needs some work before it's ready for production) devicesd package that will spin up a simple server, just so we could take a peek at our apiv1 endpoints as they'd actually be used. (It worked. Yay?) We should continue to expand on this with configuration, more information being logged, etc.

History
1 package devices
3 import (
4 "fmt"
5 "testing"
6 "time"
8 "code.secondbit.org/uuid.hg"
9 "golang.org/x/net/context"
10 )
12 type StorerFactory interface {
13 NewStorer(ctx context.Context) (Storer, error)
14 TeardownStorer(storer Storer, ctx context.Context) error
15 }
17 var storerFactories []StorerFactory
19 const (
20 changeName = 1 << iota
21 changeOwner
22 changeType
23 changeLastSeen
24 changePushToken
25 changeVariations
26 )
28 func compareDevices(device1, device2 Device) (ok bool, field string, expected, result interface{}) {
29 if !device1.ID.Equal(device2.ID) {
30 return false, "ID", device1.ID, device2.ID
31 }
32 if device1.Name != device2.Name {
33 return false, "Name", device1.Name, device2.Name
34 }
35 if !device1.Owner.Equal(device2.Owner) {
36 return false, "Owner", device1.Owner, device2.Owner
37 }
38 if device1.Type != device2.Type {
39 return false, "Type", device1.Type, device2.Type
40 }
41 if !device1.Created.Equal(device2.Created) {
42 return false, "Created", device1.Created, device2.Created
43 }
44 if !device1.LastSeen.Equal(device2.LastSeen) {
45 return false, "LastSeen", device1.LastSeen, device2.LastSeen
46 }
47 if device1.PushToken != device2.PushToken {
48 return false, "PushToken", device1.PushToken, device2.PushToken
49 }
50 return true, "", nil, nil
51 }
53 func TestCreateAndGetDevices(t *testing.T) {
54 for _, factory := range storerFactories {
55 ctx := context.Background()
56 storer, err := factory.NewStorer(ctx)
57 if err != nil {
58 t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
59 }
61 devices := []Device{
62 {ID: uuid.NewID(), Name: "Test 1", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
63 {ID: uuid.NewID(), Name: "Test 2", Owner: uuid.NewID(), Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
64 {ID: uuid.NewID(), Name: "Test 3", Owner: uuid.NewID(), Type: TypeChromeExtension, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
65 }
67 err = storer.CreateDevices(devices, ctx)
68 if err != nil {
69 t.Errorf("Error creating devices in %T: %+v\n", storer, err)
70 }
72 ids := make([]uuid.ID, 0, len(devices))
73 for _, device := range devices {
74 ids = append(ids, device.ID)
75 }
77 results, err := storer.GetDevices(ids, ctx)
78 if err != nil {
79 t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
80 }
81 for _, device := range devices {
82 d, returned := results[device.ID.String()]
83 if !returned {
84 t.Errorf("Expected device %s to be in results from %T, but wasn't present\n", device.Name, storer)
85 }
86 ok, field, expected, result := compareDevices(device, d)
87 if !ok {
88 t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, device.Name, expected, result, storer)
89 }
90 }
91 err = factory.TeardownStorer(storer, ctx)
92 if err != nil {
93 t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
94 }
95 }
96 }
98 func TestGetDevicesNoErrorForMissing(t *testing.T) {
99 for _, factory := range storerFactories {
100 ctx := context.Background()
101 storer, err := factory.NewStorer(ctx)
102 if err != nil {
103 t.Fatalf("Fatal error creatng Storer from %T: %+v\n", factory, err)
104 }
106 results, err := storer.GetDevices([]uuid.ID{uuid.NewID()}, ctx)
107 if err != nil {
108 t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
109 }
110 if len(results) != 0 {
111 t.Errorf("Expected results to be empty, got %+v from %T instead\n", results, storer)
112 }
113 err = factory.TeardownStorer(storer, ctx)
114 if err != nil {
115 t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
116 }
117 }
118 }
120 func TestCreateDevicesDuplicates(t *testing.T) {
121 for _, factory := range storerFactories {
122 ctx := context.Background()
123 storer, err := factory.NewStorer(ctx)
124 if err != nil {
125 t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
126 }
128 devices := []Device{
129 {ID: uuid.NewID(), Name: "Test 1", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
130 {ID: uuid.NewID(), Name: "Test 2", Owner: uuid.NewID(), Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
131 {ID: uuid.NewID(), Name: "Test 3", Owner: uuid.NewID(), Type: TypeChromeExtension, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
132 }
134 err = storer.CreateDevices(devices, ctx)
135 if err != nil {
136 t.Errorf("Unexpected error creating devices in %T: %+v\n", storer, err)
137 }
139 newDevices := []Device{
140 {ID: uuid.NewID(), Name: "Test 4", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
141 {ID: uuid.NewID(), Name: "Test 5", Owner: uuid.NewID(), Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
142 }
144 err = storer.CreateDevices([]Device{newDevices[0], devices[1], newDevices[1]}, ctx)
145 daeErr, ok := err.(ErrDeviceAlreadyExists)
146 if !ok {
147 t.Errorf("Expected ErrDeviceAlreadyExists creating duplicate device in %T, got %+v\n", storer, err)
148 }
149 if !uuid.ID(daeErr).Equal(devices[1].ID) {
150 t.Errorf("Expected ErrDeviceAlreadyExists to be %+v, got %+v from %T\n", devices[1].ID, daeErr, storer)
151 }
153 // inserts should be a transaction; they either all make it, or none do
154 results, err := storer.GetDevices([]uuid.ID{newDevices[0].ID, newDevices[1].ID}, ctx)
155 if err != nil {
156 t.Errorf("Error retrieving devices from %T: %+v\n", storer, err)
157 }
158 if len(results) != 0 {
159 t.Errorf("Expected new inserts to not be in results, got %+v from %T\n", results, storer)
160 }
162 err = factory.TeardownStorer(storer, ctx)
163 if err != nil {
164 t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
165 }
166 }
167 }
169 func TestCreateAndListDevicesByOwner(t *testing.T) {
170 for _, factory := range storerFactories {
171 ctx := context.Background()
172 storer, err := factory.NewStorer(ctx)
173 if err != nil {
174 t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
175 }
177 owner1, owner2 := uuid.NewID(), uuid.NewID()
178 devices := []Device{
179 {ID: uuid.NewID(), Name: "Test 1", Owner: owner1, Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
180 {ID: uuid.NewID(), Name: "Test 2", Owner: owner2, Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
181 {ID: uuid.NewID(), Name: "Test 3", Owner: owner1, Type: TypeChromeExtension, Created: time.Now().Add(time.Minute), LastSeen: time.Now(), PushToken: "test token"},
182 }
184 err = storer.CreateDevices(devices, ctx)
185 if err != nil {
186 t.Errorf("Error creating devices in %T: %+v\n", storer, err)
187 }
189 results, err := storer.ListDevicesByOwner(owner1, ctx)
190 if err != nil {
191 t.Errorf("Error listing devices for owner1 from %T: %+v\n", storer, err)
192 }
193 if len(results) != 2 {
194 t.Errorf("Expected %d results for owner1, got %d from %T\n", 2, len(results), storer)
195 }
196 resultMap := ToMap(results)
197 d, ok := resultMap[devices[0].ID.String()]
198 if !ok {
199 t.Errorf("Expected to get %s in results, got %+v from %T\n", devices[0].Name, results, storer)
200 }
201 ok, field, expected, result := compareDevices(devices[0], d)
202 if !ok {
203 t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, devices[0].Name, expected, result, storer)
204 }
205 d, ok = resultMap[devices[2].ID.String()]
206 if !ok {
207 t.Errorf("Expected to get %s in results, got %+v from %T\n", devices[2].Name, results, storer)
208 }
209 ok, field, expected, result = compareDevices(devices[2], d)
210 if !ok {
211 t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, devices[2].Name, expected, result, storer)
212 }
214 results, err = storer.ListDevicesByOwner(owner2, ctx)
215 if err != nil {
216 t.Errorf("Error listing devices for owner2 from %T: %+v\n", storer, err)
217 }
218 if len(results) != 1 {
219 t.Errorf("Expected %d results for owner2, got %d from %T\n", 1, len(results), storer)
220 }
221 ok, field, expected, result = compareDevices(devices[1], results[0])
222 if !ok {
223 t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, devices[1].Name, expected, result, storer)
224 }
226 err = factory.TeardownStorer(storer, ctx)
227 if err != nil {
228 t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
229 }
230 }
231 }
233 func TestUpdateDevicesHappyPath(t *testing.T) {
234 device := Device{
235 ID: uuid.NewID(),
236 Name: "Test 1",
237 Owner: uuid.NewID(),
238 Type: TypeAndroidPhone,
239 Created: time.Now(),
240 LastSeen: time.Now(),
241 PushToken: "test token",
242 }
243 for _, factory := range storerFactories {
244 ctx := context.Background()
245 storer, err := factory.NewStorer(ctx)
246 if err != nil {
247 t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
248 }
249 for i := 1; i < changeVariations; i++ {
250 var change DeviceChange
251 var owner uuid.ID
252 var name, pushToken string
253 var lastSeen time.Time
254 var deviceType DeviceType
256 device.ID = uuid.NewID()
258 expectation := device
259 result := device
261 change.DeviceID = device.ID
263 if i&changeName != 0 {
264 name = fmt.Sprintf("name-%d", i)
265 change.Name = &name
266 expectation.Name = name
267 }
268 if i&changeOwner != 0 {
269 owner = uuid.NewID()
270 change.Owner = &owner
271 expectation.Owner = owner
272 }
273 if i&changeType != 0 {
274 deviceType = DeviceType(fmt.Sprintf("type-%d", i))
275 change.Type = &deviceType
276 expectation.Type = deviceType
277 }
278 if i&changeLastSeen != 0 {
279 lastSeen = time.Now().Add(time.Minute * time.Duration(i*-1))
280 change.LastSeen = &lastSeen
281 expectation.LastSeen = lastSeen
282 }
283 if i&changePushToken != 0 {
284 pushToken = fmt.Sprintf("push-token-%d", i)
285 change.PushToken = &pushToken
286 expectation.PushToken = pushToken
287 }
288 result = ApplyChange(result, change)
289 ok, field, expectedVal, resultVal := compareDevices(expectation, result)
290 if !ok {
291 t.Errorf("Expected %s of %s to be %v, got %v after applying DeviceChange %+v\n", field, device.Name, expectedVal, resultVal, change)
292 }
294 err = storer.CreateDevices([]Device{device}, ctx)
295 if err != nil {
296 t.Errorf("Unexpected error creating devices in %T: %+v\n", storer, err)
297 }
299 err = storer.UpdateDevice(change, ctx)
300 if err != nil {
301 t.Errorf("Unexpected error updating device in %T: %+v\n", storer, err)
302 }
304 retrieved, err := storer.GetDevices([]uuid.ID{device.ID}, ctx)
305 if err != nil {
306 t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
307 }
308 retrievedDevice, ok := retrieved[device.ID.String()]
309 if !ok {
310 t.Errorf("Expected retrieved devices to contain %s, got %+v from %T\n", device.Name, retrieved, storer)
311 }
312 ok, field, expectedVal, resultVal = compareDevices(expectation, retrievedDevice)
313 if !ok {
314 t.Errorf("Expected %s of %s to be %v, got %v from %T\n", field, device.Name, expectedVal, resultVal, storer)
315 }
316 }
318 err = factory.TeardownStorer(storer, ctx)
319 if err != nil {
320 t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
321 }
322 }
323 }
325 func TestUpdateDeviceNotFound(t *testing.T) {
326 for _, factory := range storerFactories {
327 ctx := context.Background()
328 storer, err := factory.NewStorer(ctx)
329 if err != nil {
330 t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
331 }
333 deviceID := uuid.NewID()
334 name := "my new name"
335 change := DeviceChange{DeviceID: deviceID, Name: &name}
337 err = storer.UpdateDevice(change, ctx)
338 if err != ErrDeviceNotFound {
339 t.Errorf("Expected error to be ErrDeviceNotFound, %T returned %+v\n", storer, err)
340 }
342 results, err := storer.GetDevices([]uuid.ID{deviceID}, ctx)
343 if err != nil {
344 t.Errorf("Error retrieving devices from %T: %+v\n", storer, err)
345 }
346 if len(results) != 0 {
347 t.Errorf("Expected no devices in %T, got %+v\n", storer, results)
348 }
350 err = factory.TeardownStorer(storer, ctx)
351 if err != nil {
352 t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
353 }
354 }
355 }
357 func TestDeleteDevicesHappyPath(t *testing.T) {
358 for _, factory := range storerFactories {
359 ctx := context.Background()
360 storer, err := factory.NewStorer(ctx)
361 if err != nil {
362 t.Fatalf("Fatal error creating Storer from %T: %+v\n", factory, err)
363 }
365 owner1, owner2 := uuid.NewID(), uuid.NewID()
367 devices := []Device{
368 {ID: uuid.NewID(), Name: "Test 1", Owner: owner1, Type: TypeAndroidPhone, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
369 {ID: uuid.NewID(), Name: "Test 2", Owner: owner2, Type: TypeAndroidTablet, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
370 {ID: uuid.NewID(), Name: "Test 3", Owner: owner1, Type: TypeChromeExtension, Created: time.Now(), LastSeen: time.Now(), PushToken: "test token"},
371 }
373 err = storer.CreateDevices(devices, ctx)
374 if err != nil {
375 t.Errorf("Error creating devices in %T: %+v\n", storer, err)
376 }
378 err = storer.DeleteDevices([]uuid.ID{devices[0].ID, devices[1].ID}, ctx)
379 if err != nil {
380 t.Errorf("Error deleting devices from %T: %+v\n", storer, err)
381 }
383 results, err := storer.GetDevices([]uuid.ID{devices[0].ID, devices[1].ID, devices[2].ID}, ctx)
384 if err != nil {
385 t.Errorf("Unexpected error retrieving devices from %T: %+v\n", storer, err)
386 }
388 if len(results) != 1 {
389 t.Errorf("Expected %d results, got %d from %T\n", 1, len(results), storer)
390 }
392 device, ok := results[devices[0].ID.String()]
393 if ok {
394 t.Errorf("Retrieved first device (which was deleted!) from %T: %+v\n", storer, device)
395 }
397 device, ok = results[devices[1].ID.String()]
398 if ok {
399 t.Errorf("Retrieved second device (which was deleted!) from %T: %+v\n", storer, device)
400 }
402 device, ok = results[devices[2].ID.String()]
403 if !ok {
404 t.Errorf("Didn't retrieve third device (which wasn't deleted!) from %T. Got %+v\n", storer, results)
405 }
407 err = factory.TeardownStorer(storer, ctx)
408 if err != nil {
409 t.Errorf("Error cleaning up after %T: %+v\n", storer, err)
410 }
411 }
412 }