ducky/subscriptions
1:f1a22fc2321d Browse Files
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.
postgres.go sql/postgres_init.sql subscription.go subscription_memstore.go subscription_postgres.go subscription_store_test.go
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/postgres.go Sun Jun 14 02:48:08 2015 -0400 1.3 @@ -0,0 +1,27 @@ 1.4 +package subscriptions 1.5 + 1.6 +import ( 1.7 + "database/sql" 1.8 +) 1.9 + 1.10 +// NewPostgres returns a usable Postgres instance, if and only 1.11 +// if error is nil. If error is not nil, the returned Postgres 1.12 +// instance should not be used. 1.13 +func NewPostgres(conn string) (Postgres, error) { 1.14 + db, err := sql.Open("postgres", conn) 1.15 + if err != nil { 1.16 + return Postgres{}, err 1.17 + } 1.18 + return Postgres{db}, nil 1.19 +} 1.20 + 1.21 +// Postgres represents a thin wrapper around *sql.DB, so we can 1.22 +// inherit its methods but also define our own (so we can fulfill 1.23 +// our subscriptionStore interface on Postgres). Note that the 1.24 +// value of a Postgres variable contains a _pointer_ to an sql.DB 1.25 +// pool of connections, so it's not necessary to use the address of 1.26 +// the Postgres variable itself (though there shouldn't be any harm 1.27 +// in doing so). 1.28 +type Postgres struct { 1.29 + *sql.DB 1.30 +}
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/sql/postgres_init.sql Sun Jun 14 02:48:08 2015 -0400 2.3 @@ -0,0 +1,11 @@ 2.4 +CREATE TABLE IF NOT EXISTS subscriptions ( 2.5 + user_id VARCHAR(36) PRIMARY KEY, 2.6 + stripe_customer VARCHAR(36) UNIQUE NOT NULL, 2.7 + amount INTEGER NOT NULL, 2.8 + period VARCHAR(16) NOT NULL, 2.9 + created TIMESTAMPTZ NOT NULL, 2.10 + begin_charging TIMESTAMPTZ NOT NULL, 2.11 + last_charged TIMESTAMPTZ NOT NULL, 2.12 + last_notified TIMESTAMPTZ NOT NULL, 2.13 + in_lockout BOOLEAN NOT NULL 2.14 +);
3.1 --- a/subscription.go Thu Jun 11 23:15:01 2015 -0400 3.2 +++ b/subscription.go Sun Jun 14 02:48:08 2015 -0400 3.3 @@ -1,6 +1,7 @@ 3.4 package subscriptions 3.5 3.6 import ( 3.7 + "database/sql/driver" 3.8 "errors" 3.9 "time" 3.10 3.11 @@ -30,16 +31,39 @@ 3.12 // ErrNoSubscriptionID is returned when one or more Subscription IDs 3.13 // are required, but none are provided. 3.14 ErrNoSubscriptionID = errors.New("no Subscription ID provided") 3.15 + // ErrPeriodInvalid is returned when attempting to use a period that 3.16 + // is not a valid period. 3.17 + ErrPeriodInvalid = errors.New("invalid period") 3.18 ) 3.19 3.20 type period string 3.21 3.22 +func (p period) Value() (driver.Value, error) { 3.23 + return string(p), nil 3.24 +} 3.25 + 3.26 +func (p *period) Scan(src interface{}) error { 3.27 + if src == nil { 3.28 + *p = period("") 3.29 + return nil 3.30 + } 3.31 + switch src.(type) { 3.32 + case []byte: 3.33 + *p = period(string(src.([]byte))) 3.34 + return nil 3.35 + case string: 3.36 + *p = period(src.(string)) 3.37 + return nil 3.38 + default: 3.39 + return ErrPeriodInvalid 3.40 + } 3.41 +} 3.42 + 3.43 // Subscription represents the state of a user's payments. It holds 3.44 // metadata about the last time a user was charged, how much a user 3.45 // should be charged, how to charge a user and how much to charge 3.46 // the user. 3.47 type Subscription struct { 3.48 - ID uuid.ID 3.49 UserID uuid.ID 3.50 StripeCustomer string 3.51 Amount int 3.52 @@ -138,6 +162,27 @@ 3.53 return s[i].LastCharged.Before(s[j].LastCharged) 3.54 } 3.55 3.56 +// SubscriptionStats represents a set of statistics about our Subscription 3.57 +// data that will be exposed to the Prometheus scraper. 3.58 +type SubscriptionStats struct { 3.59 + Number int64 3.60 + TotalAmount int64 3.61 + MeanAmount float64 3.62 + // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service. 3.63 + // Because of this, we can only report stats that will be identical across nodes, e.g. stats 3.64 + // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666 3.65 + // In the future, we'll need per-node metrics. For now, we'll make do. 3.66 + // 3.67 + // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set: 3.68 + // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port, 3.69 + // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local. 3.70 + // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local. 3.71 + // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and 3.72 + // contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local 3.73 + // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment 3.74 + // was omitted." 3.75 +} 3.76 + 3.77 type subscriptionStore interface { 3.78 reset() error 3.79 createSubscription(sub Subscription) error 3.80 @@ -145,5 +190,5 @@ 3.81 deleteSubscription(id uuid.ID) error 3.82 listSubscriptionsLastChargedBefore(time.Time) ([]Subscription, error) 3.83 getSubscriptions(ids []uuid.ID) (map[string]Subscription, error) 3.84 - getSubscriptionByUser(id uuid.ID) (Subscription, error) 3.85 + getSubscriptionStats() (SubscriptionStats, error) 3.86 }
4.1 --- a/subscription_memstore.go Thu Jun 11 23:15:01 2015 -0400 4.2 +++ b/subscription_memstore.go Sun Jun 14 02:48:08 2015 -0400 4.3 @@ -20,13 +20,13 @@ 4.4 m.subscriptionLock.Lock() 4.5 defer m.subscriptionLock.Unlock() 4.6 4.7 - if _, ok := m.subscriptions[sub.ID.String()]; ok { 4.8 + if _, ok := m.subscriptions[sub.UserID.String()]; ok { 4.9 return ErrSubscriptionAlreadyExists 4.10 } 4.11 if stripeCustomerInMemstore(sub.StripeCustomer, m) { 4.12 return ErrStripeCustomerAlreadyExists 4.13 } 4.14 - m.subscriptions[sub.ID.String()] = sub 4.15 + m.subscriptions[sub.UserID.String()] = sub 4.16 return nil 4.17 } 4.18 4.19 @@ -97,20 +97,24 @@ 4.20 if !ok { 4.21 continue 4.22 } 4.23 - result[s.ID.String()] = s 4.24 + result[s.UserID.String()] = s 4.25 } 4.26 return result, nil 4.27 } 4.28 4.29 -func (m *Memstore) getSubscriptionByUser(id uuid.ID) (Subscription, error) { 4.30 +func (m *Memstore) getSubscriptionStats() (SubscriptionStats, error) { 4.31 m.subscriptionLock.RLock() 4.32 defer m.subscriptionLock.RUnlock() 4.33 4.34 + stats := SubscriptionStats{} 4.35 + 4.36 for _, subscription := range m.subscriptions { 4.37 - if subscription.UserID.Equal(id) { 4.38 - return subscription, nil 4.39 - } 4.40 + stats.Number++ 4.41 + stats.TotalAmount += int64(subscription.Amount) 4.42 } 4.43 4.44 - return Subscription{}, ErrSubscriptionNotFound 4.45 + if stats.Number > 0 { 4.46 + stats.MeanAmount = float64(stats.TotalAmount) / float64(stats.Number) 4.47 + } 4.48 + return stats, nil 4.49 }
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 5.2 +++ b/subscription_postgres.go Sun Jun 14 02:48:08 2015 -0400 5.3 @@ -0,0 +1,220 @@ 5.4 +package subscriptions 5.5 + 5.6 +import ( 5.7 + "database/sql" 5.8 + "time" 5.9 + 5.10 + "code.secondbit.org/uuid.hg" 5.11 + "github.com/lib/pq" 5.12 + "github.com/secondbit/pan" 5.13 +) 5.14 + 5.15 +// GetSQLTableName fulfills the pan.SQLTableNamer interface, allowing 5.16 +// us to manipulate Subscriptions with pan. 5.17 +func (s Subscription) GetSQLTableName() string { 5.18 + return "subscriptions" 5.19 +} 5.20 + 5.21 +func (p Postgres) resetSQL() *pan.Query { 5.22 + var sub Subscription 5.23 + query := pan.New(pan.POSTGRES, "TRUNCATE "+pan.GetTableName(sub)) 5.24 + return query.FlushExpressions(" ") 5.25 +} 5.26 + 5.27 +func (p Postgres) reset() error { 5.28 + query := p.resetSQL() 5.29 + _, err := p.Exec(query.String(), query.Args...) 5.30 + if err != nil { 5.31 + return err 5.32 + } 5.33 + return nil 5.34 +} 5.35 + 5.36 +func (p Postgres) createSubscriptionSQL(sub Subscription) *pan.Query { 5.37 + fields, values := pan.GetFields(sub) 5.38 + query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(sub)) 5.39 + query.Include("(" + pan.QueryList(fields) + ")") 5.40 + query.Include("VALUES") 5.41 + query.Include("("+pan.VariableList(len(values))+")", values...) 5.42 + return query.FlushExpressions(" ") 5.43 +} 5.44 + 5.45 +func (p Postgres) createSubscription(sub Subscription) error { 5.46 + query := p.createSubscriptionSQL(sub) 5.47 + _, err := p.Exec(query.String(), query.Args...) 5.48 + if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_pkey" { 5.49 + err = ErrSubscriptionAlreadyExists 5.50 + } else if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_customer_key" { 5.51 + err = ErrStripeCustomerAlreadyExists 5.52 + } 5.53 + return err 5.54 +} 5.55 + 5.56 +func (p Postgres) updateSubscriptionSQL(id uuid.ID, change SubscriptionChange) *pan.Query { 5.57 + var sub Subscription 5.58 + query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(sub)+" SET") 5.59 + query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "StripeCustomer")+" = ?", change.StripeCustomer) 5.60 + query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "Amount")+" = ?", change.Amount) 5.61 + query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "Period")+" = ?", change.Period) 5.62 + query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "BeginCharging")+" = ?", change.BeginCharging) 5.63 + query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "LastCharged")+" = ?", change.LastCharged) 5.64 + query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "LastNotified")+" = ?", change.LastNotified) 5.65 + query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "InLockout")+" = ?", change.InLockout) 5.66 + query.FlushExpressions(", ") 5.67 + query.IncludeWhere() 5.68 + query.Include(pan.GetUnquotedColumn(sub, "UserID")+" = ?", id) 5.69 + return query.FlushExpressions(" ") 5.70 +} 5.71 + 5.72 +func (p Postgres) updateSubscription(id uuid.ID, change SubscriptionChange) error { 5.73 + if change.IsEmpty() { 5.74 + return ErrSubscriptionChangeEmpty 5.75 + } 5.76 + 5.77 + query := p.updateSubscriptionSQL(id, change) 5.78 + res, err := p.Exec(query.String(), query.Args...) 5.79 + if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_customer_key" { 5.80 + return ErrStripeCustomerAlreadyExists 5.81 + } else if err != nil { 5.82 + return err 5.83 + } 5.84 + rows, err := res.RowsAffected() 5.85 + if err != nil { 5.86 + return err 5.87 + } 5.88 + if rows < 1 { 5.89 + return ErrSubscriptionNotFound 5.90 + } 5.91 + return nil 5.92 +} 5.93 + 5.94 +func (p Postgres) deleteSubscriptionSQL(id uuid.ID) *pan.Query { 5.95 + var sub Subscription 5.96 + query := pan.New(pan.POSTGRES, "DELETE FROM "+pan.GetTableName(sub)) 5.97 + query.IncludeWhere() 5.98 + query.Include(pan.GetUnquotedColumn(sub, "UserID")+" = ?", id) 5.99 + return query.FlushExpressions(" ") 5.100 +} 5.101 + 5.102 +func (p Postgres) deleteSubscription(id uuid.ID) error { 5.103 + query := p.deleteSubscriptionSQL(id) 5.104 + res, err := p.Exec(query.String(), query.Args...) 5.105 + if err != nil { 5.106 + return err 5.107 + } 5.108 + rows, err := res.RowsAffected() 5.109 + if err != nil { 5.110 + return err 5.111 + } 5.112 + if rows < 1 { 5.113 + return ErrSubscriptionNotFound 5.114 + } 5.115 + return nil 5.116 +} 5.117 + 5.118 +func (p Postgres) listSubscriptionsLastChargedBeforeSQL(cutoff time.Time) *pan.Query { 5.119 + var sub Subscription 5.120 + fields, _ := pan.GetFields(sub) 5.121 + query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(sub)) 5.122 + query.IncludeWhere() 5.123 + query.Include(pan.GetUnquotedColumn(sub, "LastCharged")+" < ?", cutoff) 5.124 + query.IncludeOrder(pan.GetUnquotedColumn(sub, "LastCharged") + " ASC") 5.125 + return query.FlushExpressions(" ") 5.126 +} 5.127 + 5.128 +func (p Postgres) listSubscriptionsLastChargedBefore(cutoff time.Time) ([]Subscription, error) { 5.129 + var results []Subscription 5.130 + query := p.listSubscriptionsLastChargedBeforeSQL(cutoff) 5.131 + rows, err := p.Query(query.String(), query.Args...) 5.132 + if err != nil { 5.133 + return results, err 5.134 + } 5.135 + for rows.Next() { 5.136 + var sub Subscription 5.137 + err := pan.Unmarshal(rows, &sub) 5.138 + if err != nil { 5.139 + return results, err 5.140 + } 5.141 + results = append(results, sub) 5.142 + } 5.143 + if err := rows.Err(); err != nil { 5.144 + return results, err 5.145 + } 5.146 + return results, nil 5.147 +} 5.148 + 5.149 +func (p Postgres) getSubscriptionsSQL(ids []uuid.ID) *pan.Query { 5.150 + var sub Subscription 5.151 + fields, _ := pan.GetFields(sub) 5.152 + intIDs := make([]interface{}, len(ids)) 5.153 + for pos, id := range ids { 5.154 + intIDs[pos] = id 5.155 + } 5.156 + query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(sub)) 5.157 + query.IncludeWhere() 5.158 + query.Include(pan.GetUnquotedColumn(sub, "UserID") + " IN") 5.159 + query.Include("("+pan.VariableList(len(intIDs))+")", intIDs...) 5.160 + return query.FlushExpressions(" ") 5.161 +} 5.162 + 5.163 +func (p Postgres) getSubscriptions(ids []uuid.ID) (map[string]Subscription, error) { 5.164 + results := map[string]Subscription{} 5.165 + if len(ids) < 1 { 5.166 + return results, ErrNoSubscriptionID 5.167 + } 5.168 + query := p.getSubscriptionsSQL(ids) 5.169 + rows, err := p.Query(query.String(), query.Args...) 5.170 + if err != nil { 5.171 + return results, err 5.172 + } 5.173 + for rows.Next() { 5.174 + var sub Subscription 5.175 + err := pan.Unmarshal(rows, &sub) 5.176 + if err != nil { 5.177 + return results, err 5.178 + } 5.179 + results[sub.UserID.String()] = sub 5.180 + } 5.181 + if err := rows.Err(); err != nil { 5.182 + return results, err 5.183 + } 5.184 + return results, nil 5.185 +} 5.186 + 5.187 +func (p Postgres) getSubscriptionStatsSQL() *pan.Query { 5.188 + var sub Subscription 5.189 + amountColumn := pan.GetUnquotedColumn(sub, "Amount") 5.190 + query := pan.New(pan.POSTGRES, "SELECT") 5.191 + query.Include("COUNT(*), SUM(" + amountColumn + "), AVG(" + amountColumn + ")") 5.192 + query.Include("FROM " + pan.GetTableName(sub)) 5.193 + return query.FlushExpressions(" ") 5.194 +} 5.195 + 5.196 +func (p Postgres) getSubscriptionStats() (SubscriptionStats, error) { 5.197 + query := p.getSubscriptionStatsSQL() 5.198 + rows, err := p.Query(query.String(), query.Args...) 5.199 + if err != nil { 5.200 + return SubscriptionStats{}, err 5.201 + } 5.202 + var stats SubscriptionStats 5.203 + for rows.Next() { 5.204 + var number, total sql.NullInt64 5.205 + var mean sql.NullFloat64 5.206 + if err := rows.Scan(number, total, mean); err != nil { 5.207 + return stats, err 5.208 + } 5.209 + if number.Valid { 5.210 + stats.Number = number.Int64 5.211 + } 5.212 + if total.Valid { 5.213 + stats.TotalAmount = total.Int64 5.214 + } 5.215 + if mean.Valid { 5.216 + stats.MeanAmount = mean.Float64 5.217 + } 5.218 + } 5.219 + if err := rows.Err(); err != nil { 5.220 + return stats, err 5.221 + } 5.222 + return stats, nil 5.223 +}
6.1 --- a/subscription_store_test.go Thu Jun 11 23:15:01 2015 -0400 6.2 +++ b/subscription_store_test.go Sun Jun 14 02:48:08 2015 -0400 6.3 @@ -1,6 +1,7 @@ 6.4 package subscriptions 6.5 6.6 import ( 6.7 + "os" 6.8 "strconv" 6.9 "testing" 6.10 "time" 6.11 @@ -18,14 +19,21 @@ 6.12 subscriptionChangeInLockout 6.13 ) 6.14 6.15 +func init() { 6.16 + if os.Getenv("PG_TEST_DB") != "" { 6.17 + p, err := NewPostgres(os.Getenv("PG_TEST_DB")) 6.18 + if err != nil { 6.19 + panic(err) 6.20 + } 6.21 + testSubscriptionStores = append(testSubscriptionStores, p) 6.22 + } 6.23 +} 6.24 + 6.25 var testSubscriptionStores = []subscriptionStore{ 6.26 NewMemstore(), 6.27 } 6.28 6.29 func compareSubscriptions(sub1, sub2 Subscription) (bool, string, interface{}, interface{}) { 6.30 - if !sub1.ID.Equal(sub2.ID) { 6.31 - return false, "ID", sub1.ID, sub2.ID 6.32 - } 6.33 if !sub1.UserID.Equal(sub2.UserID) { 6.34 return false, "UserID", sub1.UserID, sub2.UserID 6.35 } 6.36 @@ -59,7 +67,7 @@ 6.37 func subscriptionMapContains(subscriptionMap map[string]Subscription, subscriptions ...Subscription) (bool, []Subscription) { 6.38 var missing []Subscription 6.39 for _, sub := range subscriptions { 6.40 - if _, ok := subscriptionMap[sub.ID.String()]; !ok { 6.41 + if _, ok := subscriptionMap[sub.UserID.String()]; !ok { 6.42 missing = append(missing, sub) 6.43 } 6.44 } 6.45 @@ -77,26 +85,25 @@ 6.46 } 6.47 customerID := uuid.NewID() 6.48 sub := Subscription{ 6.49 - ID: uuid.NewID(), 6.50 UserID: customerID, 6.51 StripeCustomer: "stripeCustomer1", 6.52 Amount: 200, 6.53 Period: MonthlyPeriod, 6.54 - Created: time.Now(), 6.55 - BeginCharging: time.Now().Add(time.Hour), 6.56 + Created: time.Now().Round(time.Millisecond), 6.57 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour), 6.58 } 6.59 err = store.createSubscription(sub) 6.60 if err != nil { 6.61 t.Errorf("Error creating subscription in %T: %+v\n", store, err) 6.62 } 6.63 - retrieved, err := store.getSubscriptions([]uuid.ID{sub.ID}) 6.64 + retrieved, err := store.getSubscriptions([]uuid.ID{sub.UserID}) 6.65 if err != nil { 6.66 t.Errorf("Error retrieving subscription from %T: %+v\n", store, err) 6.67 } 6.68 - if _, returned := retrieved[sub.ID.String()]; !returned { 6.69 - t.Errorf("Error retrieving subscription from %T: %s wasn't in the results.", store, sub.ID) 6.70 + if _, returned := retrieved[sub.UserID.String()]; !returned { 6.71 + t.Errorf("Error retrieving subscription from %T: %s wasn't in the results.", store, sub.UserID) 6.72 } 6.73 - ok, field, expected, result := compareSubscriptions(sub, retrieved[sub.ID.String()]) 6.74 + ok, field, expected, result := compareSubscriptions(sub, retrieved[sub.UserID.String()]) 6.75 if !ok { 6.76 t.Errorf("Expected %s to be %v, got %v from %T\n", field, expected, result, store) 6.77 } 6.78 @@ -104,10 +111,10 @@ 6.79 if err != ErrSubscriptionAlreadyExists { 6.80 t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrSubscriptionAlreadyExists, err) 6.81 } 6.82 - sub.ID = uuid.NewID() 6.83 + sub.UserID = uuid.NewID() 6.84 err = store.createSubscription(sub) 6.85 if err != ErrStripeCustomerAlreadyExists { 6.86 - t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrStripeCustomerAlreadyExists, err) 6.87 + t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %#+v\n", store, ErrStripeCustomerAlreadyExists, err) 6.88 } 6.89 sub.StripeCustomer = "stripeCustomer2" 6.90 err = store.createSubscription(sub) 6.91 @@ -120,27 +127,25 @@ 6.92 func TestUpdateSubscription(t *testing.T) { 6.93 variations := 1 << 7 6.94 sub := Subscription{ 6.95 - ID: uuid.NewID(), 6.96 UserID: uuid.NewID(), 6.97 StripeCustomer: "default", 6.98 Amount: -1, 6.99 Period: MonthlyPeriod, 6.100 - Created: time.Now().Add(time.Hour * -24), 6.101 - BeginCharging: time.Now().Add(time.Hour * -24), 6.102 - LastCharged: time.Now().Add(time.Hour * -24), 6.103 - LastNotified: time.Now().Add(time.Hour * -24), 6.104 + Created: time.Now().Round(time.Millisecond).Add(time.Hour * -24), 6.105 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -24), 6.106 + LastCharged: time.Now().Round(time.Millisecond).Add(time.Hour * -24), 6.107 + LastNotified: time.Now().Round(time.Millisecond).Add(time.Hour * -24), 6.108 InLockout: true, 6.109 } 6.110 sub2 := Subscription{ 6.111 - ID: uuid.NewID(), 6.112 UserID: uuid.NewID(), 6.113 StripeCustomer: "stripeCustomer2", 6.114 Amount: -2, 6.115 Period: MonthlyPeriod, 6.116 - Created: time.Now(), 6.117 - BeginCharging: time.Now(), 6.118 - LastCharged: time.Now(), 6.119 - LastNotified: time.Now(), 6.120 + Created: time.Now().Round(time.Millisecond), 6.121 + BeginCharging: time.Now().Round(time.Millisecond), 6.122 + LastCharged: time.Now().Round(time.Millisecond), 6.123 + LastNotified: time.Now().Round(time.Millisecond), 6.124 InLockout: false, 6.125 } 6.126 6.127 @@ -179,19 +184,19 @@ 6.128 } 6.129 6.130 if i&subscriptionChangeBeginCharging != 0 { 6.131 - beginCharging = time.Now().Add(time.Hour * time.Duration(i)) 6.132 + beginCharging = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i)) 6.133 change.BeginCharging = &beginCharging 6.134 expectation.BeginCharging = beginCharging 6.135 } 6.136 6.137 if i&subscriptionChangeLastCharged != 0 { 6.138 - lastCharged = time.Now().Add(time.Hour * time.Duration(i)) 6.139 + lastCharged = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i)) 6.140 change.LastCharged = &lastCharged 6.141 expectation.LastCharged = lastCharged 6.142 } 6.143 6.144 if i&subscriptionChangeLastNotified != 0 { 6.145 - lastNotified = time.Now().Add(time.Hour * time.Duration(i)) 6.146 + lastNotified = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i)) 6.147 change.LastNotified = &lastNotified 6.148 expectation.LastNotified = lastNotified 6.149 } 6.150 @@ -221,19 +226,19 @@ 6.151 if err != nil { 6.152 t.Fatalf("Error saving subscription in %T: %s\n", store, err) 6.153 } 6.154 - err = store.updateSubscription(sub.ID, change) 6.155 + err = store.updateSubscription(sub.UserID, change) 6.156 if err != nil { 6.157 t.Errorf("Error updating subscription in %T: %s\n", store, err) 6.158 } 6.159 - retrieved, err := store.getSubscriptions([]uuid.ID{sub.ID}) 6.160 + retrieved, err := store.getSubscriptions([]uuid.ID{sub.UserID}) 6.161 if err != nil { 6.162 t.Errorf("Error getting subscription from %T: %s\n", store, err) 6.163 } 6.164 ok, missing := subscriptionMapContains(retrieved, sub) 6.165 if !ok { 6.166 - t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub.ID.String(), store, missing) 6.167 + t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub.UserID.String(), store, missing) 6.168 } 6.169 - match, field, expected, got = compareSubscriptions(expectation, retrieved[sub.ID.String()]) 6.170 + match, field, expected, got = compareSubscriptions(expectation, retrieved[sub.UserID.String()]) 6.171 if !match { 6.172 t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T\n", field, expected, got, store) 6.173 } 6.174 @@ -253,7 +258,7 @@ 6.175 t.Fatalf("Error saving subscription in %T: %+v\n", store, err) 6.176 } 6.177 change := SubscriptionChange{} 6.178 - err = store.updateSubscription(sub.ID, change) 6.179 + err = store.updateSubscription(sub.UserID, change) 6.180 if err != ErrSubscriptionChangeEmpty { 6.181 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionChangeEmpty, err, store) 6.182 } 6.183 @@ -263,7 +268,7 @@ 6.184 if err != ErrSubscriptionNotFound { 6.185 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store) 6.186 } 6.187 - err = store.updateSubscription(sub.ID, change) 6.188 + err = store.updateSubscription(sub.UserID, change) 6.189 if err != ErrStripeCustomerAlreadyExists { 6.190 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrStripeCustomerAlreadyExists, err, store) 6.191 } 6.192 @@ -277,12 +282,10 @@ 6.193 t.Fatalf("Error resetting %T: %+v\n", store, err) 6.194 } 6.195 sub1 := Subscription{ 6.196 - ID: uuid.NewID(), 6.197 UserID: uuid.NewID(), 6.198 StripeCustomer: "stripeCustomer1", 6.199 } 6.200 sub2 := Subscription{ 6.201 - ID: uuid.NewID(), 6.202 UserID: uuid.NewID(), 6.203 StripeCustomer: "stripeCustomer2", 6.204 } 6.205 @@ -294,27 +297,23 @@ 6.206 if err != nil { 6.207 t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err) 6.208 } 6.209 - err = store.deleteSubscription(sub1.ID) 6.210 + err = store.deleteSubscription(sub1.UserID) 6.211 if err != nil { 6.212 t.Fatalf("Error deleting %+v in %T: %+v\n", sub1, store, err) 6.213 } 6.214 - retrieved, err := store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID}) 6.215 + retrieved, err := store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID}) 6.216 if err != nil { 6.217 t.Errorf("Error retrieving subscriptions from %T: %+v\n", store, err) 6.218 } 6.219 ok, missing := subscriptionMapContains(retrieved, sub1) 6.220 if ok { 6.221 - t.Errorf("Expected not to retrieve %s from %T, but missing was %+v\n", sub1.ID.String(), store, missing) 6.222 + t.Errorf("Expected not to retrieve %s from %T, but missing was %+v\n", sub1.UserID.String(), store, missing) 6.223 } 6.224 ok, missing = subscriptionMapContains(retrieved, sub2) 6.225 if !ok { 6.226 - t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub2.ID.String(), store, missing) 6.227 + t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub2.UserID.String(), store, missing) 6.228 } 6.229 - _, err = store.getSubscriptionByUser(sub1.UserID) 6.230 - if err != ErrSubscriptionNotFound { 6.231 - t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store) 6.232 - } 6.233 - err = store.deleteSubscription(sub1.ID) 6.234 + err = store.deleteSubscription(sub1.UserID) 6.235 if err != ErrSubscriptionNotFound { 6.236 t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store) 6.237 } 6.238 @@ -328,33 +327,30 @@ 6.239 t.Fatalf("Error resetting %T: %+v\n", store, err) 6.240 } 6.241 sub1 := Subscription{ 6.242 - ID: uuid.NewID(), 6.243 UserID: uuid.NewID(), 6.244 StripeCustomer: "stripeCustomer1", 6.245 Amount: 200, 6.246 Period: MonthlyPeriod, 6.247 - Created: time.Now().Add(time.Hour * -24 * 32), 6.248 - BeginCharging: time.Now().Add(time.Hour * -24), 6.249 - LastCharged: time.Now().Add(time.Hour * -24), 6.250 + Created: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 32), 6.251 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -24), 6.252 + LastCharged: time.Now().Round(time.Millisecond).Add(time.Hour * -24), 6.253 } 6.254 sub2 := Subscription{ 6.255 - ID: uuid.NewID(), 6.256 UserID: uuid.NewID(), 6.257 StripeCustomer: "stripeCustomer2", 6.258 Amount: 300, 6.259 Period: MonthlyPeriod, 6.260 - Created: time.Now().Add(time.Hour * -24 * 61), 6.261 - BeginCharging: time.Now().Add(time.Hour * -24 * 31), 6.262 - LastCharged: time.Now().Add(time.Hour * -24 * 31), 6.263 + Created: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 61), 6.264 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 31), 6.265 + LastCharged: time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 31), 6.266 } 6.267 sub3 := Subscription{ 6.268 - ID: uuid.NewID(), 6.269 UserID: uuid.NewID(), 6.270 StripeCustomer: "stripeCustomer3", 6.271 Amount: 100, 6.272 Period: MonthlyPeriod, 6.273 - Created: time.Now().Add(time.Hour * -1), 6.274 - BeginCharging: time.Now().Add(time.Hour * 31), 6.275 + Created: time.Now().Round(time.Millisecond).Add(time.Hour * -1), 6.276 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * 31), 6.277 LastCharged: time.Time{}, 6.278 } 6.279 err = store.createSubscription(sub1) 6.280 @@ -374,7 +370,7 @@ 6.281 t.Logf("sub3: %+v\n", sub3) 6.282 // subscriptions last charged before right now 6.283 // should be sub1, sub2, and sub3 6.284 - results, err := store.listSubscriptionsLastChargedBefore(time.Now()) 6.285 + results, err := store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond)) 6.286 if err != nil { 6.287 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err) 6.288 } 6.289 @@ -395,7 +391,7 @@ 6.290 } 6.291 // subscriptions last charged before a week ago 6.292 // should be sub2, sub3 6.293 - results, err = store.listSubscriptionsLastChargedBefore(time.Now().Add(time.Hour * -24 * 7)) 6.294 + results, err = store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 7)) 6.295 if err != nil { 6.296 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err) 6.297 } 6.298 @@ -412,7 +408,7 @@ 6.299 } 6.300 // subscriptions last charged before 32 days ago 6.301 // should be sub3 6.302 - results, err = store.listSubscriptionsLastChargedBefore(time.Now().Add(time.Hour * -24 * 32)) 6.303 + results, err = store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 32)) 6.304 if err != nil { 6.305 t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err) 6.306 } 6.307 @@ -433,33 +429,30 @@ 6.308 t.Fatalf("Error resetting %T: %+v\n", store, err) 6.309 } 6.310 sub1 := Subscription{ 6.311 - ID: uuid.NewID(), 6.312 UserID: uuid.NewID(), 6.313 StripeCustomer: "stripeCustomer1", 6.314 Amount: 200, 6.315 Period: MonthlyPeriod, 6.316 - Created: time.Now(), 6.317 - BeginCharging: time.Now().Add(time.Hour), 6.318 + Created: time.Now().Round(time.Millisecond), 6.319 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour), 6.320 } 6.321 sub2 := Subscription{ 6.322 - ID: uuid.NewID(), 6.323 UserID: uuid.NewID(), 6.324 StripeCustomer: "stripeCustomer2", 6.325 Amount: 300, 6.326 Period: MonthlyPeriod, 6.327 - Created: time.Now().Add(time.Hour * -720), 6.328 - BeginCharging: time.Now().Add(time.Hour*-720 + time.Hour*2), 6.329 - LastCharged: time.Now(), 6.330 + Created: time.Now().Round(time.Millisecond).Add(time.Hour * -720), 6.331 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour*-720 + time.Hour*2), 6.332 + LastCharged: time.Now().Round(time.Millisecond), 6.333 } 6.334 sub3 := Subscription{ 6.335 - ID: uuid.NewID(), 6.336 UserID: uuid.NewID(), 6.337 StripeCustomer: "stripeCustomer3", 6.338 Amount: 100, 6.339 Period: MonthlyPeriod, 6.340 - Created: time.Now().Add(time.Hour * -1440), 6.341 - BeginCharging: time.Now().Add(time.Hour * -1440), 6.342 - LastNotified: time.Now().Add(time.Hour * -720), 6.343 + Created: time.Now().Round(time.Millisecond).Add(time.Hour * -1440), 6.344 + BeginCharging: time.Now().Round(time.Millisecond).Add(time.Hour * -1440), 6.345 + LastNotified: time.Now().Round(time.Millisecond).Add(time.Hour * -720), 6.346 InLockout: true, 6.347 } 6.348 err = store.createSubscription(sub1) 6.349 @@ -478,63 +471,63 @@ 6.350 if err != ErrNoSubscriptionID { 6.351 t.Errorf("Error retrieving no subscriptions from %T. Expected %+v, got %+v\n", store, ErrNoSubscriptionID, err) 6.352 } 6.353 - retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID}) 6.354 + retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID}) 6.355 if err != nil { 6.356 - t.Errorf("Error retrieving %s from %T: %+v\n", sub1.ID, store, err) 6.357 + t.Errorf("Error retrieving %s from %T: %+v\n", sub1.UserID, store, err) 6.358 } 6.359 ok, missing := subscriptionMapContains(retrieved, sub1) 6.360 if !ok { 6.361 t.Logf("Results: %+v\n", retrieved) 6.362 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store) 6.363 } 6.364 - retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID}) 6.365 + retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID}) 6.366 if err != nil { 6.367 - t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.ID, sub2.ID, store, err) 6.368 + t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub2.UserID, store, err) 6.369 } 6.370 ok, missing = subscriptionMapContains(retrieved, sub1, sub2) 6.371 if !ok { 6.372 t.Logf("Results: %+v\n", retrieved) 6.373 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store) 6.374 } 6.375 - retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub3.ID}) 6.376 + retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub3.UserID}) 6.377 if err != nil { 6.378 - t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.ID, sub3.ID, store, err) 6.379 + t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub3.UserID, store, err) 6.380 } 6.381 ok, missing = subscriptionMapContains(retrieved, sub1, sub3) 6.382 if !ok { 6.383 t.Logf("Results: %+v\n", retrieved) 6.384 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store) 6.385 } 6.386 - retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID, sub3.ID}) 6.387 + retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, sub3.UserID}) 6.388 if err != nil { 6.389 - t.Errorf("Error retrieving %s, %s, and %s from %T: %+v\n", sub1.ID, sub2.ID, sub3.ID, store, err) 6.390 + t.Errorf("Error retrieving %s, %s, and %s from %T: %+v\n", sub1.UserID, sub2.UserID, sub3.UserID, store, err) 6.391 } 6.392 ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3) 6.393 if !ok { 6.394 t.Logf("Results: %+v\n", retrieved) 6.395 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store) 6.396 } 6.397 - retrieved, err = store.getSubscriptions([]uuid.ID{sub2.ID}) 6.398 + retrieved, err = store.getSubscriptions([]uuid.ID{sub2.UserID}) 6.399 if err != nil { 6.400 - t.Errorf("Error retrieving %s from %T: %+v\n", sub2.ID, store, err) 6.401 + t.Errorf("Error retrieving %s from %T: %+v\n", sub2.UserID, store, err) 6.402 } 6.403 ok, missing = subscriptionMapContains(retrieved, sub2) 6.404 if !ok { 6.405 t.Logf("Results: %+v\n", retrieved) 6.406 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store) 6.407 } 6.408 - retrieved, err = store.getSubscriptions([]uuid.ID{sub2.ID, sub3.ID}) 6.409 + retrieved, err = store.getSubscriptions([]uuid.ID{sub2.UserID, sub3.UserID}) 6.410 if err != nil { 6.411 - t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub2.ID, sub3.ID, store, err) 6.412 + t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub2.UserID, sub3.UserID, store, err) 6.413 } 6.414 ok, missing = subscriptionMapContains(retrieved, sub2, sub3) 6.415 if !ok { 6.416 t.Logf("Results: %+v\n", retrieved) 6.417 t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store) 6.418 } 6.419 - retrieved, err = store.getSubscriptions([]uuid.ID{sub3.ID}) 6.420 + retrieved, err = store.getSubscriptions([]uuid.ID{sub3.UserID}) 6.421 if err != nil { 6.422 - t.Errorf("Error retrieving %s from %T: %+v\n", sub3.ID, store, err) 6.423 + t.Errorf("Error retrieving %s from %T: %+v\n", sub3.UserID, store, err) 6.424 } 6.425 ok, missing = subscriptionMapContains(retrieved, sub3) 6.426 if !ok { 6.427 @@ -548,7 +541,7 @@ 6.428 if len(retrieved) != 0 { 6.429 t.Errorf("Expected no results, %T returned %+v\n", store, retrieved) 6.430 } 6.431 - retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID, uuid.NewID(), sub3.ID}) 6.432 + retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, uuid.NewID(), sub3.UserID}) 6.433 if err != nil { 6.434 t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err) 6.435 } 6.436 @@ -562,67 +555,3 @@ 6.437 } 6.438 } 6.439 } 6.440 - 6.441 -func TestGetSubscriptionByUser(t *testing.T) { 6.442 - for _, store := range testSubscriptionStores { 6.443 - err := store.reset() 6.444 - if err != nil { 6.445 - t.Fatalf("Error resetting %T: %+v\n", store, err) 6.446 - } 6.447 - sub1 := Subscription{ 6.448 - ID: uuid.NewID(), 6.449 - UserID: uuid.NewID(), 6.450 - StripeCustomer: "stripeCustomer1", 6.451 - } 6.452 - sub2 := Subscription{ 6.453 - ID: uuid.NewID(), 6.454 - UserID: uuid.NewID(), 6.455 - StripeCustomer: "stripeCustomer2", 6.456 - } 6.457 - sub3 := Subscription{ 6.458 - ID: uuid.NewID(), 6.459 - UserID: uuid.NewID(), 6.460 - StripeCustomer: "stripeCustomer3", 6.461 - } 6.462 - err = store.createSubscription(sub1) 6.463 - if err != nil { 6.464 - t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err) 6.465 - } 6.466 - err = store.createSubscription(sub2) 6.467 - if err != nil { 6.468 - t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err) 6.469 - } 6.470 - err = store.createSubscription(sub3) 6.471 - if err != nil { 6.472 - t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err) 6.473 - } 6.474 - retrieved, err := store.getSubscriptionByUser(sub1.UserID) 6.475 - if err != nil { 6.476 - t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub1, store, err) 6.477 - } 6.478 - ok, field, expected, result := compareSubscriptions(sub1, retrieved) 6.479 - if !ok { 6.480 - t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store) 6.481 - } 6.482 - retrieved, err = store.getSubscriptionByUser(sub2.UserID) 6.483 - if err != nil { 6.484 - t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub2, store, err) 6.485 - } 6.486 - ok, field, expected, result = compareSubscriptions(sub2, retrieved) 6.487 - if !ok { 6.488 - t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store) 6.489 - } 6.490 - retrieved, err = store.getSubscriptionByUser(sub3.UserID) 6.491 - if err != nil { 6.492 - t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub3, store, err) 6.493 - } 6.494 - ok, field, expected, result = compareSubscriptions(sub3, retrieved) 6.495 - if !ok { 6.496 - t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store) 6.497 - } 6.498 - retrieved, err = store.getSubscriptionByUser(uuid.NewID()) 6.499 - if err != ErrSubscriptionNotFound { 6.500 - t.Errorf("Expected err to be %+v, got %+v from %T\n", ErrSubscriptionNotFound, err, store) 6.501 - } 6.502 - } 6.503 -}