Log Postgres test failures more verbosely, fix SubscriptionChange.IsEmpty.
SubscriptionChange.IsEmpty() would return false even if no actual database
operations are going to be performed. This is because we allow information we
_don't_ store in the database (Stripe source, Stripe email) to be specified in a
SubscriptionChange object, just so we can easily access them. Then we use the
Stripe API to store them in Stripe's databases, and turn them into data _we_
store in our database. Think of them as pre-processed values that are never
stored raw.
The problem is, we were treating these properties the same as the properties we
actually stored in the database, and (worse) were running database tests for
combinations of these properties, which was causing test failures because we
were trying to update no columns in the database. Whoops.
I removed these properties from the IsEmpty helper, and removed them from the
code that generates the SubscriptionChange permutations for testing. This allows
tests to pass, but also stays closer to what the system was designed to do.
In tracking down this bug, I discovered that the logging we had for errors when
running Postgres tests was inadequate, so I updated the logs when that failure
occurs while testing Postgres to help surface future failures faster.
13 commonAPI "code.secondbit.org/api.hg"
14 "code.secondbit.org/uuid.hg"
16 "code.secondbit.org/ducky/subscriptions.hg/api"
20 ErrNilClient = errors.New("nil client wrapper")
21 ErrNilHTTPClient = errors.New("nil client")
30 func New(address string, id uuid.ID) *Client {
31 address = strings.TrimRight(address, "/")
34 client: &http.Client{},
39 func (c *Client) do(method, url string, request interface{}, scopes []string, subject uuid.ID) (api.Response, error) {
41 return api.Response{}, ErrNilClient
44 return api.Response{}, ErrNilHTTPClient
46 var response api.Response
47 if !strings.HasPrefix(url, "http") {
48 url = strings.TrimLeft(url, "/")
54 data, err := json.Marshal(request)
58 body = bytes.NewBuffer(data)
60 req, err := http.NewRequest(method, url, body)
64 req.Header.Set("Ducky-Scope", strings.Join(scopes, " "))
65 req.Header.Set("Ducky-Issuer", c.ID.String())
67 req.Header.Set("Ducky-Subject", subject.String())
69 req.Header.Set("Ducky-Expires", time.Now().Add(time.Hour).String())
70 req.Header.Set("Ducky-Issued-At", time.Now().String())
71 req.Header.Set("Ducky-Not-Before", time.Now().Add(-5*time.Minute).String())
72 resp, err := c.client.Do(req)
76 defer resp.Body.Close()
77 switch resp.Header.Get("Content-Type") {
78 case "application/json":
79 dec := json.NewDecoder(resp.Body)
80 err = dec.Decode(&response)
85 dec := json.NewDecoder(resp.Body)
86 err = dec.Decode(&response)
91 if len(response.Errors) > 0 {
92 return response, httpErrors(response.Errors)
97 type httpErrors []commonAPI.RequestError
99 func (h httpErrors) Error() string {
100 return fmt.Sprintf("%+#v", h)
103 func (c *Client) Get(url string, scopes []string, subject uuid.ID) (api.Response, error) {
104 return c.do("GET", url, nil, scopes, subject)
107 func (c *Client) Post(url string, request interface{}, scopes []string, subject uuid.ID) (api.Response, error) {
108 return c.do("POST", url, request, scopes, subject)
111 func (c *Client) Patch(url string, request interface{}, scopes []string, subject uuid.ID) (api.Response, error) {
112 return c.do("PATCH", url, request, scopes, subject)