Create a listener that will create subscriptions.
We need a listener (as discussed in c4cfceb2f2fb) that will create a
Subscription whenever an auth.Profile is created. This is the beginning of that
effort. It hasn't been tested, and all the pieces aren't in place, but it's a
rough skeleton.
We have a Dockerfile that will correctly build a minimal container for the
listener.
We have a build-docker.sh script that will correctly build a binary that will be
used in the Dockerfile.
We have a ca-certificates.crt, which are pulled from Ubuntu, and are necessary
before we can safely consume SSL endpoints.
We created a small consumer script that listens for events off NSQ, and calls
the appropriate endpoint for our Subscriptions API. This is untested, and it
doesn't build at the moment, but that's awaiting changes in the
code.secondbit.org/auth.hg package.
Finally, we have a wrapper.sh file that will expose the Stripe secret key being
used from a Kubernetes secret file as an environment variable, instead.
9 "code.secondbit.org/uuid.hg"
13 subscriptionChangeStripeSource = 1 << iota
14 subscriptionChangeEmail
15 subscriptionChangeStripeSubscription
16 subscriptionChangePlan
17 subscriptionChangeStatus
18 subscriptionChangeCanceling
19 subscriptionChangeTrialStart
20 subscriptionChangeTrialEnd
21 subscriptionChangePeriodStart
22 subscriptionChangePeriodEnd
23 subscriptionChangeCanceledAt
24 subscriptionChangeFailedChargeAttempts
25 subscriptionChangeLastFailedCharge
26 subscriptionChangeLastNotified
30 if os.Getenv("PG_TEST_DB") != "" {
31 p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
35 testSubscriptionStores = append(testSubscriptionStores, p)
39 var testSubscriptionStores = []SubscriptionStore{
43 func compareSubscriptions(sub1, sub2 Subscription) (bool, string, interface{}, interface{}) {
44 if !sub1.UserID.Equal(sub2.UserID) {
45 return false, "UserID", sub1.UserID, sub2.UserID
47 if sub1.StripeSubscription != sub2.StripeSubscription {
48 return false, "StripeSubscription", sub1.StripeSubscription, sub2.StripeSubscription
50 if sub1.Plan != sub2.Plan {
51 return false, "Plan", sub1.Plan, sub2.Plan
53 if sub1.Status != sub2.Status {
54 return false, "Status", sub1.Status, sub2.Status
56 if sub1.Canceling != sub2.Canceling {
57 return false, "Canceling", sub1.Canceling, sub2.Canceling
59 if !sub1.Created.Equal(sub2.Created) {
60 return false, "Created", sub1.Created, sub2.Created
62 if !sub1.TrialStart.Equal(sub2.TrialStart) {
63 return false, "TrialStart", sub1.TrialStart, sub2.TrialStart
65 if !sub1.TrialEnd.Equal(sub2.TrialEnd) {
66 return false, "TrialEnd", sub1.TrialEnd, sub2.TrialEnd
68 if !sub1.PeriodStart.Equal(sub2.PeriodStart) {
69 return false, "PeriodStart", sub1.PeriodStart, sub2.PeriodStart
71 if !sub1.PeriodEnd.Equal(sub2.PeriodEnd) {
72 return false, "PeriodEnd", sub1.PeriodEnd, sub2.PeriodEnd
74 if !sub1.CanceledAt.Equal(sub2.CanceledAt) {
75 return false, "CanceledAt", sub1.CanceledAt, sub2.CanceledAt
77 if sub1.FailedChargeAttempts != sub2.FailedChargeAttempts {
78 return false, "FailedChargeAttempts", sub1.FailedChargeAttempts, sub2.FailedChargeAttempts
80 if !sub1.LastFailedCharge.Equal(sub2.LastFailedCharge) {
81 return false, "LastFailedCharge", sub1.LastFailedCharge, sub2.LastFailedCharge
83 if !sub1.LastNotified.Equal(sub2.LastNotified) {
84 return false, "LastNotified", sub1.LastNotified, sub2.LastNotified
86 return true, "", nil, nil
89 func subscriptionMapContains(subscriptionMap map[string]Subscription, subscriptions ...Subscription) (bool, []Subscription) {
90 var missing []Subscription
91 for _, sub := range subscriptions {
92 if _, ok := subscriptionMap[sub.UserID.String()]; !ok {
93 missing = append(missing, sub)
102 func compareSubscriptionStats(stat1, stat2 SubscriptionStats) (bool, string, interface{}, interface{}) {
103 if stat1.Number != stat2.Number {
104 return false, "Number", stat1.Number, stat2.Number
106 if stat1.Canceling != stat2.Canceling {
107 return false, "Canceling", stat1.Canceling, stat2.Canceling
109 if stat1.Failing != stat2.Failing {
110 return false, "Failing", stat1.Failing, stat2.Failing
112 if len(stat1.Plans) != len(stat2.Plans) {
113 return false, "Plans", stat1.Plans, stat2.Plans
115 for key, count := range stat1.Plans {
116 count2, ok := stat2.Plans[key]
118 return false, "Plans", stat1.Plans, stat2.Plans
121 return false, "Plans", stat1.Plans, stat2.Plans
124 return true, "", nil, nil
127 func TestCreateSubscription(t *testing.T) {
128 for _, store := range testSubscriptionStores {
131 t.Fatalf("Error resetting %T: %+v\n", store, err)
133 customerID := uuid.NewID()
136 StripeSubscription: "stripeSubscription1",
137 Created: time.Now().Round(time.Millisecond),
138 TrialStart: time.Now().Round(time.Millisecond),
139 TrialEnd: time.Now().Round(time.Millisecond).Add(time.Hour * 24 * 31),
141 err = store.CreateSubscription(sub)
143 t.Errorf("Error creating subscription in %T: %+v\n", store, err)
145 retrieved, err := store.GetSubscriptions([]uuid.ID{sub.UserID})
147 t.Errorf("Error retrieving subscription from %T: %+v\n", store, err)
149 if _, returned := retrieved[sub.UserID.String()]; !returned {
150 t.Errorf("Error retrieving subscription from %T: %s wasn't in the results.", store, sub.UserID)
152 ok, field, expected, result := compareSubscriptions(sub, retrieved[sub.UserID.String()])
154 t.Errorf("Expected %s to be %v, got %v from %T\n", field, expected, result, store)
156 err = store.CreateSubscription(sub)
157 if err != ErrSubscriptionAlreadyExists {
158 t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrSubscriptionAlreadyExists, err)
160 sub.UserID = uuid.NewID()
161 err = store.CreateSubscription(sub)
162 if err != ErrStripeSubscriptionAlreadyExists {
163 t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %#+v\n", store, ErrStripeSubscriptionAlreadyExists, err)
165 sub.StripeSubscription = "stripeSubscription2"
166 err = store.CreateSubscription(sub)
168 t.Errorf("Error creating subscription in %T: %+v\n", store, err)
173 func TestUpdateSubscription(t *testing.T) {
174 variations := 1 << 14
176 UserID: uuid.NewID(),
177 StripeSubscription: "default",
178 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * -32),
179 TrialStart: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * -32),
180 TrialEnd: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
181 LastNotified: time.Now().Round(time.Millisecond).Add(time.Hour * -24),
183 sub2 := Subscription{
184 UserID: uuid.NewID(),
185 StripeSubscription: "stripeSubscription2",
186 Created: time.Now().Round(time.Millisecond),
187 TrialStart: time.Now().Round(time.Millisecond),
188 TrialEnd: time.Now().Round(time.Millisecond),
189 LastNotified: time.Now().Round(time.Millisecond),
192 for _, store := range testSubscriptionStores {
195 t.Fatalf("Error resetting %T: %+v\n", store, err)
197 err = store.CreateSubscription(sub)
199 t.Fatalf("Error saving subscription in %T: %s\n", store, err)
201 for i := 1; i < variations; i++ {
202 var stripeSource, email, stripeSubscription, plan, status string
204 var failedChargeAttempts int
205 var trialStart, trialEnd, periodStart, periodEnd, canceledAt, lastFailedCharge, lastNotified time.Time
207 change := SubscriptionChange{}
208 empty := change.IsEmpty()
210 t.Errorf("Expected empty to be %t, was %t\n", true, empty)
213 strI := strconv.Itoa(i)
215 if i&subscriptionChangeStripeSource != 0 {
216 stripeSource = "stripeSource-" + strI
217 change.StripeSource = &stripeSource
220 if i&subscriptionChangeEmail != 0 {
221 email = "email-" + strI
222 change.Email = &email
225 if i&subscriptionChangeStripeSubscription != 0 {
226 stripeSubscription = "stripeSubscription-" + strI
227 change.StripeSubscription = &stripeSubscription
228 sub.StripeSubscription = stripeSubscription
231 if i&subscriptionChangePlan != 0 {
232 plan = "plan-" + strI
237 if i&subscriptionChangeStatus != 0 {
238 status = "status-" + strI
239 change.Status = &status
243 if i&subscriptionChangeCanceling != 0 {
245 change.Canceling = &canceling
246 sub.Canceling = canceling
249 if i&subscriptionChangeTrialStart != 0 {
250 trialStart = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
251 change.TrialStart = &trialStart
252 sub.TrialStart = trialStart
255 if i&subscriptionChangeTrialEnd != 0 {
256 trialEnd = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
257 change.TrialEnd = &trialEnd
258 sub.TrialEnd = trialEnd
261 if i&subscriptionChangePeriodStart != 0 {
262 periodStart = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
263 change.PeriodStart = &periodStart
264 sub.PeriodStart = periodStart
267 if i&subscriptionChangePeriodEnd != 0 {
268 periodEnd = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
269 change.PeriodEnd = &periodEnd
270 sub.PeriodEnd = periodEnd
273 if i&subscriptionChangeCanceledAt != 0 {
274 canceledAt = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
275 change.CanceledAt = &canceledAt
276 sub.CanceledAt = canceledAt
279 if i&subscriptionChangeFailedChargeAttempts != 0 {
280 failedChargeAttempts = i
281 change.FailedChargeAttempts = &failedChargeAttempts
282 sub.FailedChargeAttempts = failedChargeAttempts
285 if i&subscriptionChangeLastFailedCharge != 0 {
286 lastFailedCharge = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
287 change.LastFailedCharge = &lastFailedCharge
288 sub.LastFailedCharge = lastFailedCharge
291 if i&subscriptionChangeLastNotified != 0 {
292 lastNotified = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
293 change.LastNotified = &lastNotified
294 sub.LastNotified = lastNotified
297 empty = change.IsEmpty()
299 t.Errorf("Expected empty to be %t, was %t\n", false, empty)
302 result.ApplyChange(change)
303 match, field, expected, got := compareSubscriptions(sub, result)
305 t.Errorf("Expected field `%s` to be `%v`, got `%v`\n", field, expected, got)
307 err = store.UpdateSubscription(sub.UserID, change)
309 t.Errorf("Error updating subscription in %T: %s\n", store, err)
311 retrieved, err := store.GetSubscriptions([]uuid.ID{sub.UserID})
313 t.Errorf("Error getting subscription from %T: %s\n", store, err)
315 ok, missing := subscriptionMapContains(retrieved, sub)
317 t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub.UserID.String(), store, missing)
319 match, field, expected, got = compareSubscriptions(sub, retrieved[sub.UserID.String()])
321 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T\n", field, expected, got, store)
326 err = store.CreateSubscription(sub2)
328 t.Fatalf("Error saving subscription in %T: %+v\n", store, err)
330 change := SubscriptionChange{}
331 err = store.UpdateSubscription(sub.UserID, change)
332 if err != ErrSubscriptionChangeEmpty {
333 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionChangeEmpty, err, store)
335 stripeSubscription := sub2.StripeSubscription
336 change.StripeSubscription = &stripeSubscription
337 err = store.UpdateSubscription(uuid.NewID(), change)
338 if err != ErrSubscriptionNotFound {
339 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
341 err = store.UpdateSubscription(sub.UserID, change)
342 if err != ErrStripeSubscriptionAlreadyExists {
343 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrStripeSubscriptionAlreadyExists, err, store)
348 func TestDeleteSubscription(t *testing.T) {
349 for _, store := range testSubscriptionStores {
352 t.Fatalf("Error resetting %T: %+v\n", store, err)
354 sub1 := Subscription{
355 UserID: uuid.NewID(),
356 StripeSubscription: "stripeSubscription1",
358 sub2 := Subscription{
359 UserID: uuid.NewID(),
360 StripeSubscription: "stripeSubscription2",
362 err = store.CreateSubscription(sub1)
364 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
366 err = store.CreateSubscription(sub2)
368 t.Fatalf("Error creating %+v in %T: %+v\n", sub2, store, err)
370 err = store.DeleteSubscription(sub1.UserID)
372 t.Fatalf("Error deleting %+v in %T: %+v\n", sub1, store, err)
374 retrieved, err := store.GetSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID})
376 t.Errorf("Error retrieving subscriptions from %T: %+v\n", store, err)
378 ok, missing := subscriptionMapContains(retrieved, sub1)
380 t.Errorf("Expected not to retrieve %s from %T, but missing was %+v\n", sub1.UserID.String(), store, missing)
382 ok, missing = subscriptionMapContains(retrieved, sub2)
384 t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub2.UserID.String(), store, missing)
386 err = store.DeleteSubscription(sub1.UserID)
387 if err != ErrSubscriptionNotFound {
388 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
393 func TestGetSubscriptions(t *testing.T) {
394 for _, store := range testSubscriptionStores {
397 t.Fatalf("Error resetting %T: %+v\n", store, err)
399 sub1 := Subscription{
400 UserID: uuid.NewID(),
401 StripeSubscription: "stripeSubscription1",
403 Created: time.Now().Round(time.Millisecond),
404 TrialStart: time.Now().Round(time.Millisecond),
405 TrialEnd: time.Now().Round(time.Millisecond).Add(time.Hour * 24 * 32),
407 sub2 := Subscription{
408 UserID: uuid.NewID(),
409 StripeSubscription: "stripeSubscription2",
411 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -720),
412 TrialStart: time.Now().Round(time.Millisecond).Add(time.Hour * -720),
413 TrialEnd: time.Now().Round(time.Millisecond),
415 sub3 := Subscription{
416 UserID: uuid.NewID(),
417 StripeSubscription: "stripeSubscription3",
419 Created: time.Now().Round(time.Millisecond).Add(time.Hour * -1440),
420 TrialStart: time.Now().Round(time.Millisecond).Add(time.Hour * -1440),
421 TrialEnd: time.Now().Round(time.Millisecond).Add(time.Hour * -720),
422 PeriodStart: time.Now().Round(time.Millisecond).Add(time.Hour * -720),
423 PeriodEnd: time.Now().Round(time.Millisecond),
426 err = store.CreateSubscription(sub1)
428 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
430 err = store.CreateSubscription(sub2)
432 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
434 err = store.CreateSubscription(sub3)
436 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
438 retrieved, err := store.GetSubscriptions([]uuid.ID{})
439 if err != ErrNoSubscriptionID {
440 t.Errorf("Error retrieving no subscriptions from %T. Expected %+v, got %+v\n", store, ErrNoSubscriptionID, err)
442 retrieved, err = store.GetSubscriptions([]uuid.ID{sub1.UserID})
444 t.Errorf("Error retrieving %s from %T: %+v\n", sub1.UserID, store, err)
446 ok, missing := subscriptionMapContains(retrieved, sub1)
448 t.Logf("Results: %+v\n", retrieved)
449 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
451 retrieved, err = store.GetSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID})
453 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub2.UserID, store, err)
455 ok, missing = subscriptionMapContains(retrieved, sub1, sub2)
457 t.Logf("Results: %+v\n", retrieved)
458 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
460 retrieved, err = store.GetSubscriptions([]uuid.ID{sub1.UserID, sub3.UserID})
462 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub3.UserID, store, err)
464 ok, missing = subscriptionMapContains(retrieved, sub1, sub3)
466 t.Logf("Results: %+v\n", retrieved)
467 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
469 retrieved, err = store.GetSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, sub3.UserID})
471 t.Errorf("Error retrieving %s, %s, and %s from %T: %+v\n", sub1.UserID, sub2.UserID, sub3.UserID, store, err)
473 ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3)
475 t.Logf("Results: %+v\n", retrieved)
476 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
478 retrieved, err = store.GetSubscriptions([]uuid.ID{sub2.UserID})
480 t.Errorf("Error retrieving %s from %T: %+v\n", sub2.UserID, store, err)
482 ok, missing = subscriptionMapContains(retrieved, sub2)
484 t.Logf("Results: %+v\n", retrieved)
485 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
487 retrieved, err = store.GetSubscriptions([]uuid.ID{sub2.UserID, sub3.UserID})
489 t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub2.UserID, sub3.UserID, store, err)
491 ok, missing = subscriptionMapContains(retrieved, sub2, sub3)
493 t.Logf("Results: %+v\n", retrieved)
494 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
496 retrieved, err = store.GetSubscriptions([]uuid.ID{sub3.UserID})
498 t.Errorf("Error retrieving %s from %T: %+v\n", sub3.UserID, store, err)
500 ok, missing = subscriptionMapContains(retrieved, sub3)
502 t.Logf("Results: %+v\n", retrieved)
503 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
505 retrieved, err = store.GetSubscriptions([]uuid.ID{uuid.NewID()})
507 t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err)
509 if len(retrieved) != 0 {
510 t.Errorf("Expected no results, %T returned %+v\n", store, retrieved)
512 retrieved, err = store.GetSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, uuid.NewID(), sub3.UserID})
514 t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err)
516 if len(retrieved) != 3 {
517 t.Errorf("Expected 3 results, %T returned %+v\n", store, retrieved)
519 ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3)
521 t.Logf("Results: %+v\n", retrieved)
522 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
527 func TestGetSubscriptionStats(t *testing.T) {
528 for _, store := range testSubscriptionStores {
531 t.Fatalf("Error resetting %T: %+v\n", store, err)
533 sub1 := Subscription{
534 UserID: uuid.NewID(),
535 StripeSubscription: "stripeSubscription1",
539 sub2 := Subscription{
540 UserID: uuid.NewID(),
541 StripeSubscription: "stripeSubscription2",
545 err = store.CreateSubscription(sub1)
547 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
549 stats, err := store.GetSubscriptionStats()
551 t.Errorf("Error getting stats from %T: %+v\n", store, err)
553 ok, field, expected, results := compareSubscriptionStats(SubscriptionStats{
557 Plans: map[string]int64{
562 t.Errorf("Expected %s to be %+v, got %+v from %T\n", field, expected, results, store)
564 err = store.CreateSubscription(sub2)
566 t.Fatalf("Error creating %+v in %T: %+v\n", sub2, store, err)
568 stats, err = store.GetSubscriptionStats()
570 t.Errorf("Error getting status from %T: %+v\n", store, err)
572 ok, field, expected, results = compareSubscriptionStats(SubscriptionStats{
576 Plans: map[string]int64{
582 t.Errorf("Expected %s to be %+v, got %+v from %T\n", field, expected, results, store)
584 err = store.DeleteSubscription(sub1.UserID)
586 t.Errorf("Error deleting subscription from %T: %+v\n", store, err)
588 stats, err = store.GetSubscriptionStats()
590 t.Errorf("Error getting status from %T: %+v\n", store, err)
592 ok, field, expected, results = compareSubscriptionStats(SubscriptionStats{
596 Plans: map[string]int64{
601 t.Errorf("Expected %s to be %+v, got %+v from %T\n", field, expected, results, store)