Implement PostgreSQL support, drop subscription IDs.
Create a Postgres object that wraps database/sql, so we can attach methods to it
and fulfill interfaces.
Create a postgres_init.sql script that will create the subscriptions table in a
PostgreSQL database.
Make our period type fulfill the driver.Valuer and driver.Scanner types, so it
can be stored in and retrieved from SQL databases.
Create a SubscriptionStats type, and add a method to our subscriptionStore
interface that will allow us to retrieve current stats about the Subscriptions
it is storing.
Deprecated the ID property of our Subscription type, and use the
Subscription.UserID property instead as our primary key. Subscriptions should be
unique per user and we generally will want to access Subscriptions in the
context of the User they belong to, so the UserID is a better primary key. This
also means we removed the getSubscriptionByUserID method (and implementations)
from our subscriptionStore, as getSubscriptions now fills that role.
Implement our getSubscriptionStats method in the memstore.
Implement the subscriptionStore interface on our new Postgres type.
Run the subscription store tests on our Postgres type, as well, if the
PG_TEST_DB environment variable is set.
Round all our timestamps in our tests to the nearest millisecond, as Postgres
silently truncates all timestamps to the nearest millisecond, and it was causing
false test failures.
Remove the tests for our getSubscriptionStoreByUser method, as that was removed.
9 "code.secondbit.org/uuid.hg"
13 subscriptionChangeStripeCustomer = 1 << iota
14 subscriptionChangeAmount
15 subscriptionChangePeriod
16 subscriptionChangeBeginCharging
17 subscriptionChangeLastCharged
18 subscriptionChangeLastNotified
19 subscriptionChangeInLockout
23 if os.Getenv("PG_TEST_DB") != "" {
24 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
28 testSubscriptionStores = append(testSubscriptionStores, p)
32 var testSubscriptionStores = []subscriptionStore{
36 func compareSubscriptions(sub1, sub2 Subscription) (bool, string, interface{}, interface{}) {
37 if !sub1.UserID.Equal(sub2.UserID) {
38 return false, "UserID", sub1.UserID, sub2.UserID
40 if sub1.StripeCustomer != sub2.StripeCustomer {
41 return false, "StripeCustomer", sub1.StripeCustomer, sub2.StripeCustomer
43 if sub1.Amount != sub2.Amount {
44 return false, "Amount", sub1.Amount, sub2.Amount
46 if sub1.Period != sub2.Period {
47 return false, "Period", sub1.Period, sub2.Period
49 if !sub1.Created.Equal(sub2.Created) {
50 return false, "Created", sub1.Created, sub2.Created
52 if !sub1.BeginCharging.Equal(sub2.BeginCharging) {
53 return false, "BeginCharging", sub1.BeginCharging, sub2.BeginCharging
55 if !sub1.LastCharged.Equal(sub2.LastCharged) {
56 return false, "LastCharged", sub1.LastCharged, sub2.LastCharged
58 if !sub1.LastNotified.Equal(sub2.LastNotified) {
59 return false, "LastNotified", sub1.LastNotified, sub2.LastNotified
61 if sub1.InLockout != sub2.InLockout {
62 return false, "InLockout", sub1.InLockout, sub2.InLockout
64 return true, "", nil, nil
67 func subscriptionMapContains(subscriptionMap map[string]Subscription, subscriptions ...Subscription) (bool, []Subscription) {
68 var missing []Subscription
69 for _, sub := range subscriptions {
70 if _, ok := subscriptionMap[sub.UserID.String()]; !ok {
71 missing = append(missing, sub)
80 func TestCreateSubscription(t *testing.T) {
81 for _, store := range testSubscriptionStores {
84 t.Fatalf("Error resetting %T: %+v\n", store, err)
86 customerID := uuid.NewID()
89 StripeCustomer: "stripeCustomer1",
91 Period: MonthlyPeriod,
92 Created: time.Now().Round(time.Millisecond),
93 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour),
95 err = store.createSubscription(sub)
97 t.Errorf("Error creating subscription in %T: %+v\n", store, err)
99 retrieved, err := store.getSubscriptions([]uuid.ID{sub.UserID})
101 t.Errorf("Error retrieving subscription from %T: %+v\n", store, err)
103 if _, returned := retrieved[sub.UserID.String()]; !returned {
104 t.Errorf("Error retrieving subscription from %T: %s wasn't in the results.", store, sub.UserID)
106 ok, field, expected, result := compareSubscriptions(sub, retrieved[sub.UserID.String()])
108 t.Errorf("Expected %s to be %v, got %v from %T\n", field, expected, result, store)
110 err = store.createSubscription(sub)
111 if err != ErrSubscriptionAlreadyExists {
112 t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrSubscriptionAlreadyExists, err)
114 sub.UserID = uuid.NewID()
115 err = store.createSubscription(sub)
116 if err != ErrStripeCustomerAlreadyExists {
117 t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %#+v\n", store, ErrStripeCustomerAlreadyExists, err)
119 sub.StripeCustomer = "stripeCustomer2"
120 err = store.createSubscription(sub)
122 t.Errorf("Error creating subscription in %T: %+v\n", store, err)
127 func TestUpdateSubscription(t *testing.T) {
130 UserID: uuid.NewID(),
131 StripeCustomer: "default",
133 Period: MonthlyPeriod,
134 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
135 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
136 LastCharged: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
137 LastNotified: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
140 sub2 := Subscription{
141 UserID: uuid.NewID(),
142 StripeCustomer: "stripeCustomer2",
144 Period: MonthlyPeriod,
145 Created: time.Now().Round(time.Millisecond),
146 BeginCharging: time.Now().Round(time.Millisecond),
147 LastCharged: time.Now().Round(time.Millisecond),
148 LastNotified: time.Now().Round(time.Millisecond),
152 for i := 1; i < variations; i++ {
153 var stripeCustomer string
157 var beginCharging, lastCharged, lastNotified time.Time
159 change := SubscriptionChange{}
160 empty := change.IsEmpty()
162 t.Errorf("Expected empty to be %t, was %t\n", true, empty)
166 strI := strconv.Itoa(i)
168 if i&subscriptionChangeStripeCustomer != 0 {
169 stripeCustomer = "stripeCustomer-" + strI
170 change.StripeCustomer = &stripeCustomer
171 expectation.StripeCustomer = stripeCustomer
174 if i&subscriptionChangeAmount != 0 {
176 change.Amount = &amount
177 expectation.Amount = amount
180 if i&subscriptionChangePeriod != 0 {
181 per = period("period-" + strI)
183 expectation.Period = per
186 if i&subscriptionChangeBeginCharging != 0 {
187 beginCharging = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
188 change.BeginCharging = &beginCharging
189 expectation.BeginCharging = beginCharging
192 if i&subscriptionChangeLastCharged != 0 {
193 lastCharged = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
194 change.LastCharged = &lastCharged
195 expectation.LastCharged = lastCharged
198 if i&subscriptionChangeLastNotified != 0 {
199 lastNotified = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
200 change.LastNotified = &lastNotified
201 expectation.LastNotified = lastNotified
204 if i&subscriptionChangeInLockout != 0 {
206 change.InLockout = &inLockout
207 expectation.InLockout = inLockout
210 empty = change.IsEmpty()
212 t.Errorf("Expected empty to be %t, was %t\n", false, empty)
215 result.ApplyChange(change)
216 match, field, expected, got := compareSubscriptions(expectation, result)
218 t.Errorf("Expected field `%s` to be `%v`, got `%v`\n", field, expected, got)
220 for _, store := range testSubscriptionStores {
223 t.Fatalf("Error resetting %T: %+v\n", store, err)
225 err = store.createSubscription(sub)
227 t.Fatalf("Error saving subscription in %T: %s\n", store, err)
229 err = store.updateSubscription(sub.UserID, change)
231 t.Errorf("Error updating subscription in %T: %s\n", store, err)
233 retrieved, err := store.getSubscriptions([]uuid.ID{sub.UserID})
235 t.Errorf("Error getting subscription from %T: %s\n", store, err)
237 ok, missing := subscriptionMapContains(retrieved, sub)
239 t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub.UserID.String(), store, missing)
241 match, field, expected, got = compareSubscriptions(expectation, retrieved[sub.UserID.String()])
243 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T\n", field, expected, got, store)
247 for _, store := range testSubscriptionStores {
250 t.Fatalf("Error resetting %T: %+v\n", store, err)
252 err = store.createSubscription(sub)
254 t.Fatalf("Error saving subscription in %T: %+v\n", store, err)
256 err = store.createSubscription(sub2)
258 t.Fatalf("Error saving subscription in %T: %+v\n", store, err)
260 change := SubscriptionChange{}
261 err = store.updateSubscription(sub.UserID, change)
262 if err != ErrSubscriptionChangeEmpty {
263 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionChangeEmpty, err, store)
265 stripeCustomer := sub2.StripeCustomer
266 change.StripeCustomer = &stripeCustomer
267 err = store.updateSubscription(uuid.NewID(), change)
268 if err != ErrSubscriptionNotFound {
269 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
271 err = store.updateSubscription(sub.UserID, change)
272 if err != ErrStripeCustomerAlreadyExists {
273 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrStripeCustomerAlreadyExists, err, store)
278 func TestDeleteSubscription(t *testing.T) {
279 for _, store := range testSubscriptionStores {
282 t.Fatalf("Error resetting %T: %+v\n", store, err)
284 sub1 := Subscription{
285 UserID: uuid.NewID(),
286 StripeCustomer: "stripeCustomer1",
288 sub2 := Subscription{
289 UserID: uuid.NewID(),
290 StripeCustomer: "stripeCustomer2",
292 err = store.createSubscription(sub1)
294 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
296 err = store.createSubscription(sub2)
298 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
300 err = store.deleteSubscription(sub1.UserID)
302 t.Fatalf("Error deleting %+v in %T: %+v\n", sub1, store, err)
304 retrieved, err := store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID})
306 t.Errorf("Error retrieving subscriptions from %T: %+v\n", store, err)
308 ok, missing := subscriptionMapContains(retrieved, sub1)
310 t.Errorf("Expected not to retrieve %s from %T, but missing was %+v\n", sub1.UserID.String(), store, missing)
312 ok, missing = subscriptionMapContains(retrieved, sub2)
314 t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub2.UserID.String(), store, missing)
316 err = store.deleteSubscription(sub1.UserID)
317 if err != ErrSubscriptionNotFound {
318 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
323 func TestListSubscriptionsLastChargedBefore(t *testing.T) {
324 for _, store := range testSubscriptionStores {
327 t.Fatalf("Error resetting %T: %+v\n", store, err)
329 sub1 := Subscription{
330 UserID: uuid.NewID(),
331 StripeCustomer: "stripeCustomer1",
333 Period: MonthlyPeriod,
334 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 32),
335 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
336 LastCharged: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
338 sub2 := Subscription{
339 UserID: uuid.NewID(),
340 StripeCustomer: "stripeCustomer2",
342 Period: MonthlyPeriod,
343 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 61),
344 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 31),
345 LastCharged: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 31),
347 sub3 := Subscription{
348 UserID: uuid.NewID(),
349 StripeCustomer: "stripeCustomer3",
351 Period: MonthlyPeriod,
352 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -1),
353 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * 31),
354 LastCharged: time.Time{},
356 err = store.createSubscription(sub1)
358 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
360 err = store.createSubscription(sub2)
362 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
364 err = store.createSubscription(sub3)
366 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
368 t.Logf("sub1: %+v\n", sub1)
369 t.Logf("sub2: %+v\n", sub2)
370 t.Logf("sub3: %+v\n", sub3)
371 // subscriptions last charged before right now
372 // should be sub1, sub2, and sub3
373 results, err := store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond))
375 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
377 if len(results) != 3 {
378 t.Errorf("Expected three results from %T, got %+v\n", store, results)
380 ok, field, expected, result := compareSubscriptions(sub3, results[0])
382 t.Errorf("Expected %s in pos 0 to be %+v, got %+v from %T", field, expected, result, store)
384 ok, field, expected, result = compareSubscriptions(sub2, results[1])
386 t.Errorf("Expected %s in pos 1 to be %+v, got %+v from %T", field, expected, result, store)
388 ok, field, expected, result = compareSubscriptions(sub1, results[2])
390 t.Errorf("Expected %s in pos 2 to be %+v, got %+v from %T", field, expected, result, store)
392 // subscriptions last charged before a week ago
393 // should be sub2, sub3
394 results, err = store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 7))
396 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
398 if len(results) != 2 {
399 t.Errorf("Expected two results from %T, got %+v\n", store, results)
401 ok, field, expected, result = compareSubscriptions(sub3, results[0])
403 t.Errorf("Expected %s in pos 0 to be %+v, got %+v from %T", field, expected, result, store)
405 ok, field, expected, result = compareSubscriptions(sub2, results[1])
407 t.Errorf("Expected %s in pos 1 to be %+v, got %+v from %T", field, expected, result, store)
409 // subscriptions last charged before 32 days ago
411 results, err = store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 32))
413 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
415 if len(results) != 1 {
416 t.Errorf("Expected one result from %T, got %+v\n", store, results)
418 ok, field, expected, result = compareSubscriptions(sub3, results[0])
420 t.Errorf("Expected %s in pos 0 to be %+v, got %+v from %T", field, expected, result, store)
425 func TestGetSubscriptions(t *testing.T) {
426 for _, store := range testSubscriptionStores {
429 t.Fatalf("Error resetting %T: %+v\n", store, err)
431 sub1 := Subscription{
432 UserID: uuid.NewID(),
433 StripeCustomer: "stripeCustomer1",
435 Period: MonthlyPeriod,
436 Created: time.Now().Round(time.Millisecond),
437 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour),
439 sub2 := Subscription{
440 UserID: uuid.NewID(),
441 StripeCustomer: "stripeCustomer2",
443 Period: MonthlyPeriod,
444 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -720),
445 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour*-720 + time.Hour*2),
446 LastCharged: time.Now().Round(time.Millisecond),
448 sub3 := Subscription{
449 UserID: uuid.NewID(),
450 StripeCustomer: "stripeCustomer3",
452 Period: MonthlyPeriod,
453 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -1440),
454 BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -1440),
455 LastNotified: time.Now().Round(time.Millisecond).Add(time.Hour * -720),
458 err = store.createSubscription(sub1)
460 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
462 err = store.createSubscription(sub2)
464 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
466 err = store.createSubscription(sub3)
468 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
470 retrieved, err := store.getSubscriptions([]uuid.ID{})
471 if err != ErrNoSubscriptionID {
472 t.Errorf("Error retrieving no subscriptions from %T. Expected %+v, got %+v\n", store, ErrNoSubscriptionID, err)
474 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID})
476 t.Errorf("Error retrieving %s from %T: %+v\n", sub1.UserID, store, err)
478 ok, missing := subscriptionMapContains(retrieved, sub1)
480 t.Logf("Results: %+v\n", retrieved)
481 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
483 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID})
485 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub2.UserID, store, err)
487 ok, missing = subscriptionMapContains(retrieved, sub1, sub2)
489 t.Logf("Results: %+v\n", retrieved)
490 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
492 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub3.UserID})
494 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub3.UserID, store, err)
496 ok, missing = subscriptionMapContains(retrieved, sub1, sub3)
498 t.Logf("Results: %+v\n", retrieved)
499 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
501 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, sub3.UserID})
503 t.Errorf("Error retrieving %s, %s, and %s from %T: %+v\n", sub1.UserID, sub2.UserID, sub3.UserID, store, err)
505 ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3)
507 t.Logf("Results: %+v\n", retrieved)
508 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
510 retrieved, err = store.getSubscriptions([]uuid.ID{sub2.UserID})
512 t.Errorf("Error retrieving %s from %T: %+v\n", sub2.UserID, store, err)
514 ok, missing = subscriptionMapContains(retrieved, sub2)
516 t.Logf("Results: %+v\n", retrieved)
517 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
519 retrieved, err = store.getSubscriptions([]uuid.ID{sub2.UserID, sub3.UserID})
521 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub2.UserID, sub3.UserID, store, err)
523 ok, missing = subscriptionMapContains(retrieved, sub2, sub3)
525 t.Logf("Results: %+v\n", retrieved)
526 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
528 retrieved, err = store.getSubscriptions([]uuid.ID{sub3.UserID})
530 t.Errorf("Error retrieving %s from %T: %+v\n", sub3.UserID, store, err)
532 ok, missing = subscriptionMapContains(retrieved, sub3)
534 t.Logf("Results: %+v\n", retrieved)
535 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
537 retrieved, err = store.getSubscriptions([]uuid.ID{uuid.NewID()})
539 t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err)
541 if len(retrieved) != 0 {
542 t.Errorf("Expected no results, %T returned %+v\n", store, retrieved)
544 retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, uuid.NewID(), sub3.UserID})
546 t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err)
548 if len(retrieved) != 3 {
549 t.Errorf("Expected 3 results, %T returned %+v\n", store, retrieved)
551 ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3)
553 t.Logf("Results: %+v\n", retrieved)
554 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)