First implementation of datastore interface.
Define Subscriptions, and the SubscriptionChange type that can modify
Subscriptions.
Define the subscriptionStore, which describes how Subscriptions are to be
created, retrieved, updated, and deleted in/from the storage backend they're
stored in.
Create a Memstore implementation of the subscriptionStore, which stores all our
data in-memory, for use in testing.
Write tests to cover the subscriptionStore interface, testing the entire
Memstore. We'll plug future storage backends into these tests, to make sure they
all behave the same, and to exercise each storage backend without requiring a
suite of tests for each.
At this point, we have 100% test coverage and no complaints from golint or go
vet. I expect it's all downhill from here.
8 "code.secondbit.org/uuid.hg"
12 subscriptionChangeStripeCustomer = 1 << iota
13 subscriptionChangeAmount
14 subscriptionChangePeriod
15 subscriptionChangeBeginCharging
16 subscriptionChangeLastCharged
17 subscriptionChangeLastNotified
18 subscriptionChangeInLockout
21 var testSubscriptionStores = []subscriptionStore{
25 func compareSubscriptions(sub1, sub2 Subscription) (bool, string, interface{}, interface{}) {
26 if !sub1.ID.Equal(sub2.ID) {
27 return false, "ID", sub1.ID, sub2.ID
29 if !sub1.UserID.Equal(sub2.UserID) {
30 return false, "UserID", sub1.UserID, sub2.UserID
32 if sub1.StripeCustomer != sub2.StripeCustomer {
33 return false, "StripeCustomer", sub1.StripeCustomer, sub2.StripeCustomer
35 if sub1.Amount != sub2.Amount {
36 return false, "Amount", sub1.Amount, sub2.Amount
38 if sub1.Period != sub2.Period {
39 return false, "Period", sub1.Period, sub2.Period
41 if !sub1.Created.Equal(sub2.Created) {
42 return false, "Created", sub1.Created, sub2.Created
44 if !sub1.BeginCharging.Equal(sub2.BeginCharging) {
45 return false, "BeginCharging", sub1.BeginCharging, sub2.BeginCharging
47 if !sub1.LastCharged.Equal(sub2.LastCharged) {
48 return false, "LastCharged", sub1.LastCharged, sub2.LastCharged
50 if !sub1.LastNotified.Equal(sub2.LastNotified) {
51 return false, "LastNotified", sub1.LastNotified, sub2.LastNotified
53 if sub1.InLockout != sub2.InLockout {
54 return false, "InLockout", sub1.InLockout, sub2.InLockout
56 return true, "", nil, nil
59 func subscriptionMapContains(subscriptionMap map[string]Subscription, subscriptions ...Subscription) (bool, []Subscription) {
60 var missing []Subscription
61 for _, sub := range subscriptions {
62 if _, ok := subscriptionMap[sub.ID.String()]; !ok {
63 missing = append(missing, sub)
72 func TestCreateSubscription(t *testing.T) {
73 for _, store := range testSubscriptionStores {
76 t.Fatalf("Error resetting %T: %+v\n", store, err)
78 customerID := uuid.NewID()
82 StripeCustomer: "stripeCustomer1",
84 Period: MonthlyPeriod,
86 BeginCharging: time.Now().Add(time.Hour),
88 err = store.createSubscription(sub)
90 t.Errorf("Error creating subscription in %T: %+v\n", store, err)
92 retrieved, err := store.getSubscriptions([]uuid.ID{sub.ID})
94 t.Errorf("Error retrieving subscription from %T: %+v\n", store, err)
96 if _, returned := retrieved[sub.ID.String()]; !returned {
97 t.Errorf("Error retrieving subscription from %T: %s wasn't in the results.", store, sub.ID)
99 ok, field, expected, result := compareSubscriptions(sub, retrieved[sub.ID.String()])
101 t.Errorf("Expected %s to be %v, got %v from %T\n", field, expected, result, store)
103 err = store.createSubscription(sub)
104 if err != ErrSubscriptionAlreadyExists {
105 t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrSubscriptionAlreadyExists, err)
107 sub.ID = uuid.NewID()
108 err = store.createSubscription(sub)
109 if err != ErrStripeCustomerAlreadyExists {
110 t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrStripeCustomerAlreadyExists, err)
112 sub.StripeCustomer = "stripeCustomer2"
113 err = store.createSubscription(sub)
115 t.Errorf("Error creating subscription in %T: %+v\n", store, err)
120 func TestUpdateSubscription(t *testing.T) {
124 UserID: uuid.NewID(),
125 StripeCustomer: "default",
127 Period: MonthlyPeriod,
128 Created: time.Now().Add(time.Hour * -24),
129 BeginCharging: time.Now().Add(time.Hour * -24),
130 LastCharged: time.Now().Add(time.Hour * -24),
131 LastNotified: time.Now().Add(time.Hour * -24),
134 sub2 := Subscription{
136 UserID: uuid.NewID(),
137 StripeCustomer: "stripeCustomer2",
139 Period: MonthlyPeriod,
141 BeginCharging: time.Now(),
142 LastCharged: time.Now(),
143 LastNotified: time.Now(),
147 for i := 1; i < variations; i++ {
148 var stripeCustomer string
152 var beginCharging, lastCharged, lastNotified time.Time
154 change := SubscriptionChange{}
155 empty := change.IsEmpty()
157 t.Errorf("Expected empty to be %t, was %t\n", true, empty)
161 strI := strconv.Itoa(i)
163 if i&subscriptionChangeStripeCustomer != 0 {
164 stripeCustomer = "stripeCustomer-" + strI
165 change.StripeCustomer = &stripeCustomer
166 expectation.StripeCustomer = stripeCustomer
169 if i&subscriptionChangeAmount != 0 {
171 change.Amount = &amount
172 expectation.Amount = amount
175 if i&subscriptionChangePeriod != 0 {
176 per = period("period-" + strI)
178 expectation.Period = per
181 if i&subscriptionChangeBeginCharging != 0 {
182 beginCharging = time.Now().Add(time.Hour * time.Duration(i))
183 change.BeginCharging = &beginCharging
184 expectation.BeginCharging = beginCharging
187 if i&subscriptionChangeLastCharged != 0 {
188 lastCharged = time.Now().Add(time.Hour * time.Duration(i))
189 change.LastCharged = &lastCharged
190 expectation.LastCharged = lastCharged
193 if i&subscriptionChangeLastNotified != 0 {
194 lastNotified = time.Now().Add(time.Hour * time.Duration(i))
195 change.LastNotified = &lastNotified
196 expectation.LastNotified = lastNotified
199 if i&subscriptionChangeInLockout != 0 {
201 change.InLockout = &inLockout
202 expectation.InLockout = inLockout
205 empty = change.IsEmpty()
207 t.Errorf("Expected empty to be %t, was %t\n", false, empty)
210 result.ApplyChange(change)
211 match, field, expected, got := compareSubscriptions(expectation, result)
213 t.Errorf("Expected field `%s` to be `%v`, got `%v`\n", field, expected, got)
215 for _, store := range testSubscriptionStores {
218 t.Fatalf("Error resetting %T: %+v\n", store, err)
220 err = store.createSubscription(sub)
222 t.Fatalf("Error saving subscription in %T: %s\n", store, err)
224 err = store.updateSubscription(sub.ID, change)
226 t.Errorf("Error updating subscription in %T: %s\n", store, err)
228 retrieved, err := store.getSubscriptions([]uuid.ID{sub.ID})
230 t.Errorf("Error getting subscription from %T: %s\n", store, err)
232 ok, missing := subscriptionMapContains(retrieved, sub)
234 t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub.ID.String(), store, missing)
236 match, field, expected, got = compareSubscriptions(expectation, retrieved[sub.ID.String()])
238 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T\n", field, expected, got, store)
242 for _, store := range testSubscriptionStores {
245 t.Fatalf("Error resetting %T: %+v\n", store, err)
247 err = store.createSubscription(sub)
249 t.Fatalf("Error saving subscription in %T: %+v\n", store, err)
251 err = store.createSubscription(sub2)
253 t.Fatalf("Error saving subscription in %T: %+v\n", store, err)
255 change := SubscriptionChange{}
256 err = store.updateSubscription(sub.ID, change)
257 if err != ErrSubscriptionChangeEmpty {
258 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionChangeEmpty, err, store)
260 stripeCustomer := sub2.StripeCustomer
261 change.StripeCustomer = &stripeCustomer
262 err = store.updateSubscription(uuid.NewID(), change)
263 if err != ErrSubscriptionNotFound {
264 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
266 err = store.updateSubscription(sub.ID, change)
267 if err != ErrStripeCustomerAlreadyExists {
268 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrStripeCustomerAlreadyExists, err, store)
273 func TestDeleteSubscription(t *testing.T) {
274 for _, store := range testSubscriptionStores {
277 t.Fatalf("Error resetting %T: %+v\n", store, err)
279 sub1 := Subscription{
281 UserID: uuid.NewID(),
282 StripeCustomer: "stripeCustomer1",
284 sub2 := Subscription{
286 UserID: uuid.NewID(),
287 StripeCustomer: "stripeCustomer2",
289 err = store.createSubscription(sub1)
291 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
293 err = store.createSubscription(sub2)
295 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
297 err = store.deleteSubscription(sub1.ID)
299 t.Fatalf("Error deleting %+v in %T: %+v\n", sub1, store, err)
301 retrieved, err := store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID})
303 t.Errorf("Error retrieving subscriptions from %T: %+v\n", store, err)
305 ok, missing := subscriptionMapContains(retrieved, sub1)
307 t.Errorf("Expected not to retrieve %s from %T, but missing was %+v\n", sub1.ID.String(), store, missing)
309 ok, missing = subscriptionMapContains(retrieved, sub2)
311 t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub2.ID.String(), store, missing)
313 _, err = store.getSubscriptionByUser(sub1.UserID)
314 if err != ErrSubscriptionNotFound {
315 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
317 err = store.deleteSubscription(sub1.ID)
318 if err != ErrSubscriptionNotFound {
319 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
324 func TestListSubscriptionsLastChargedBefore(t *testing.T) {
325 for _, store := range testSubscriptionStores {
328 t.Fatalf("Error resetting %T: %+v\n", store, err)
330 sub1 := Subscription{
332 UserID: uuid.NewID(),
333 StripeCustomer: "stripeCustomer1",
335 Period: MonthlyPeriod,
336 Created: time.Now().Add(time.Hour * -24 * 32),
337 BeginCharging: time.Now().Add(time.Hour * -24),
338 LastCharged: time.Now().Add(time.Hour * -24),
340 sub2 := Subscription{
342 UserID: uuid.NewID(),
343 StripeCustomer: "stripeCustomer2",
345 Period: MonthlyPeriod,
346 Created: time.Now().Add(time.Hour * -24 * 61),
347 BeginCharging: time.Now().Add(time.Hour * -24 * 31),
348 LastCharged: time.Now().Add(time.Hour * -24 * 31),
350 sub3 := Subscription{
352 UserID: uuid.NewID(),
353 StripeCustomer: "stripeCustomer3",
355 Period: MonthlyPeriod,
356 Created: time.Now().Add(time.Hour * -1),
357 BeginCharging: time.Now().Add(time.Hour * 31),
358 LastCharged: time.Time{},
360 err = store.createSubscription(sub1)
362 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
364 err = store.createSubscription(sub2)
366 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
368 err = store.createSubscription(sub3)
370 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
372 t.Logf("sub1: %+v\n", sub1)
373 t.Logf("sub2: %+v\n", sub2)
374 t.Logf("sub3: %+v\n", sub3)
375 // subscriptions last charged before right now
376 // should be sub1, sub2, and sub3
377 results, err := store.listSubscriptionsLastChargedBefore(time.Now())
379 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
381 if len(results) != 3 {
382 t.Errorf("Expected three results from %T, got %+v\n", store, results)
384 ok, field, expected, result := compareSubscriptions(sub3, results[0])
386 t.Errorf("Expected %s in pos 0 to be %+v, got %+v from %T", field, expected, result, store)
388 ok, field, expected, result = compareSubscriptions(sub2, results[1])
390 t.Errorf("Expected %s in pos 1 to be %+v, got %+v from %T", field, expected, result, store)
392 ok, field, expected, result = compareSubscriptions(sub1, results[2])
394 t.Errorf("Expected %s in pos 2 to be %+v, got %+v from %T", field, expected, result, store)
396 // subscriptions last charged before a week ago
397 // should be sub2, sub3
398 results, err = store.listSubscriptionsLastChargedBefore(time.Now().Add(time.Hour * -24 * 7))
400 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
402 if len(results) != 2 {
403 t.Errorf("Expected two results from %T, got %+v\n", store, results)
405 ok, field, expected, result = compareSubscriptions(sub3, results[0])
407 t.Errorf("Expected %s in pos 0 to be %+v, got %+v from %T", field, expected, result, store)
409 ok, field, expected, result = compareSubscriptions(sub2, results[1])
411 t.Errorf("Expected %s in pos 1 to be %+v, got %+v from %T", field, expected, result, store)
413 // subscriptions last charged before 32 days ago
415 results, err = store.listSubscriptionsLastChargedBefore(time.Now().Add(time.Hour * -24 * 32))
417 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
419 if len(results) != 1 {
420 t.Errorf("Expected one result from %T, got %+v\n", store, results)
422 ok, field, expected, result = compareSubscriptions(sub3, results[0])
424 t.Errorf("Expected %s in pos 0 to be %+v, got %+v from %T", field, expected, result, store)
429 func TestGetSubscriptions(t *testing.T) {
430 for _, store := range testSubscriptionStores {
433 t.Fatalf("Error resetting %T: %+v\n", store, err)
435 sub1 := Subscription{
437 UserID: uuid.NewID(),
438 StripeCustomer: "stripeCustomer1",
440 Period: MonthlyPeriod,
442 BeginCharging: time.Now().Add(time.Hour),
444 sub2 := Subscription{
446 UserID: uuid.NewID(),
447 StripeCustomer: "stripeCustomer2",
449 Period: MonthlyPeriod,
450 Created: time.Now().Add(time.Hour * -720),
451 BeginCharging: time.Now().Add(time.Hour*-720 + time.Hour*2),
452 LastCharged: time.Now(),
454 sub3 := Subscription{
456 UserID: uuid.NewID(),
457 StripeCustomer: "stripeCustomer3",
459 Period: MonthlyPeriod,
460 Created: time.Now().Add(time.Hour * -1440),
461 BeginCharging: time.Now().Add(time.Hour * -1440),
462 LastNotified: time.Now().Add(time.Hour * -720),
465 err = store.createSubscription(sub1)
467 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
469 err = store.createSubscription(sub2)
471 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
473 err = store.createSubscription(sub3)
475 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
477 retrieved, err := store.getSubscriptions([]uuid.ID{})
478 if err != ErrNoSubscriptionID {
479 t.Errorf("Error retrieving no subscriptions from %T. Expected %+v, got %+v\n", store, ErrNoSubscriptionID, err)
481 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID})
483 t.Errorf("Error retrieving %s from %T: %+v\n", sub1.ID, store, err)
485 ok, missing := subscriptionMapContains(retrieved, sub1)
487 t.Logf("Results: %+v\n", retrieved)
488 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
490 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID})
492 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.ID, sub2.ID, store, err)
494 ok, missing = subscriptionMapContains(retrieved, sub1, sub2)
496 t.Logf("Results: %+v\n", retrieved)
497 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
499 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub3.ID})
501 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.ID, sub3.ID, store, err)
503 ok, missing = subscriptionMapContains(retrieved, sub1, sub3)
505 t.Logf("Results: %+v\n", retrieved)
506 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
508 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID, sub3.ID})
510 t.Errorf("Error retrieving %s, %s, and %s from %T: %+v\n", sub1.ID, sub2.ID, sub3.ID, store, err)
512 ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3)
514 t.Logf("Results: %+v\n", retrieved)
515 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
517 retrieved, err = store.getSubscriptions([]uuid.ID{sub2.ID})
519 t.Errorf("Error retrieving %s from %T: %+v\n", sub2.ID, store, err)
521 ok, missing = subscriptionMapContains(retrieved, sub2)
523 t.Logf("Results: %+v\n", retrieved)
524 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
526 retrieved, err = store.getSubscriptions([]uuid.ID{sub2.ID, sub3.ID})
528 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub2.ID, sub3.ID, store, err)
530 ok, missing = subscriptionMapContains(retrieved, sub2, sub3)
532 t.Logf("Results: %+v\n", retrieved)
533 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
535 retrieved, err = store.getSubscriptions([]uuid.ID{sub3.ID})
537 t.Errorf("Error retrieving %s from %T: %+v\n", sub3.ID, store, err)
539 ok, missing = subscriptionMapContains(retrieved, sub3)
541 t.Logf("Results: %+v\n", retrieved)
542 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
544 retrieved, err = store.getSubscriptions([]uuid.ID{uuid.NewID()})
546 t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err)
548 if len(retrieved) != 0 {
549 t.Errorf("Expected no results, %T returned %+v\n", store, retrieved)
551 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID, uuid.NewID(), sub3.ID})
553 t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err)
555 if len(retrieved) != 3 {
556 t.Errorf("Expected 3 results, %T returned %+v\n", store, retrieved)
558 ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3)
560 t.Logf("Results: %+v\n", retrieved)
561 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
566 func TestGetSubscriptionByUser(t *testing.T) {
567 for _, store := range testSubscriptionStores {
570 t.Fatalf("Error resetting %T: %+v\n", store, err)
572 sub1 := Subscription{
574 UserID: uuid.NewID(),
575 StripeCustomer: "stripeCustomer1",
577 sub2 := Subscription{
579 UserID: uuid.NewID(),
580 StripeCustomer: "stripeCustomer2",
582 sub3 := Subscription{
584 UserID: uuid.NewID(),
585 StripeCustomer: "stripeCustomer3",
587 err = store.createSubscription(sub1)
589 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
591 err = store.createSubscription(sub2)
593 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
595 err = store.createSubscription(sub3)
597 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
599 retrieved, err := store.getSubscriptionByUser(sub1.UserID)
601 t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub1, store, err)
603 ok, field, expected, result := compareSubscriptions(sub1, retrieved)
605 t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store)
607 retrieved, err = store.getSubscriptionByUser(sub2.UserID)
609 t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub2, store, err)
611 ok, field, expected, result = compareSubscriptions(sub2, retrieved)
613 t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store)
615 retrieved, err = store.getSubscriptionByUser(sub3.UserID)
617 t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub3, store, err)
619 ok, field, expected, result = compareSubscriptions(sub3, retrieved)
621 t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store)
623 retrieved, err = store.getSubscriptionByUser(uuid.NewID())
624 if err != ErrSubscriptionNotFound {
625 t.Errorf("Expected err to be %+v, got %+v from %T\n", ErrSubscriptionNotFound, err, store)