diff --git a/api/api.go b/api/api.go index 3cf68fb..f3213cb 100644 --- a/api/api.go +++ b/api/api.go @@ -188,7 +188,10 @@ func (a *API) orderRoutes(r *router) { r.With(addGetBody).Post("/", a.PaymentCreate) }) - r.Get("/downloads", a.DownloadList) + r.Route("/downloads", func(r *router) { + r.Get("/", a.DownloadList) + r.Post("/refresh", a.DownloadRefresh) + }) r.Get("/receipt", a.ReceiptView) r.Post("/receipt", a.ResendOrderReceipt) }) diff --git a/api/download.go b/api/download.go index 564a4cc..c19ebd3 100644 --- a/api/download.go +++ b/api/download.go @@ -83,7 +83,6 @@ func (a *API) DownloadList(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() orderID := gcontext.GetOrderID(ctx) log := getLogEntry(r) - claims := gcontext.GetClaims(ctx) order := &models.Order{} if orderID != "" { @@ -114,6 +113,7 @@ func (a *API) DownloadList(w http.ResponseWriter, r *http.Request) error { if order != nil { query = query.Where(orderTable+".id = ?", order.ID) } else { + claims := gcontext.GetClaims(ctx) query = query.Where(orderTable+".user_id = ?", claims.Subject) } @@ -130,3 +130,44 @@ func (a *API) DownloadList(w http.ResponseWriter, r *http.Request) error { log.WithField("download_count", len(downloads)).Debugf("Successfully retrieved %d downloads", len(downloads)) return sendJSON(w, http.StatusOK, downloads) } + +// DownloadRefresh makes sure downloads are up to date +func (a *API) DownloadRefresh(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + orderID := gcontext.GetOrderID(ctx) + config := gcontext.GetConfig(ctx) + log := getLogEntry(r) + + order := &models.Order{} + if orderID == "" { + return badRequestError("Order id missing") + } + + query := a.db.Where("id = ?", orderID). + Preload("LineItems"). + Preload("Downloads") + if result := query.First(order); result.Error != nil { + if result.RecordNotFound() { + return notFoundError("Download order not found") + } + return internalServerError("Error during database query").WithInternalError(result.Error) + } + + if !hasOrderAccess(ctx, order) { + return unauthorizedError("You don't have permission to access this order") + } + + if order.PaymentState != models.PaidState { + return unauthorizedError("This order has not been completed yet") + } + + if err := order.UpdateDownloads(config, log); err != nil { + return internalServerError("Error during updating downloads").WithInternalError(err) + } + + if result := a.db.Save(order); result.Error != nil { + return internalServerError("Error during saving order").WithInternalError(result.Error) + } + + return sendJSON(w, http.StatusOK, map[string]string{}) +} diff --git a/api/download_test.go b/api/download_test.go index 96c4ed9..c3267af 100644 --- a/api/download_test.go +++ b/api/download_test.go @@ -1,7 +1,11 @@ package api import ( + "encoding/json" + "fmt" + "io/ioutil" "net/http" + "net/http/httptest" "testing" "github.com/netlify/gocommerce/models" @@ -19,3 +23,70 @@ func TestDownloadList(t *testing.T) { assert.Len(t, downloads, 1) }) } + +func currentDownloads(test *RouteTest) []models.Download { + recorder := test.TestEndpoint(http.MethodGet, "/downloads", nil, test.Data.testUserToken) + + downloads := []models.Download{} + extractPayload(test.T, http.StatusOK, recorder, &downloads) + return downloads +} + +type DownloadMeta struct { + Title string `json:"title"` + URL string `json:"url"` +} + +func startTestSiteWithDownloads(t *testing.T, downloads []*DownloadMeta) *httptest.Server { + downloadsList, err := json.Marshal(downloads) + assert.NoError(t, err) + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/i/believe/i/can/fly": + fmt.Fprintf(w, productMetaFrame(` + {"sku": "123-i-can-fly-456", "downloads": %s}`), + string(downloadsList), + ) + } + })) +} + +func TestDownloadRefresh(t *testing.T) { + test := NewRouteTest(t) + downloadsBefore := currentDownloads(test) + + testSite := startTestSiteWithDownloads(t, []*DownloadMeta{ + &DownloadMeta{ + Title: "Updated Download", + URL: "/my/special/new/url", + }, + }) + defer testSite.Close() + test.Config.SiteURL = testSite.URL + + url := fmt.Sprintf("/orders/%s/downloads/refresh", test.Data.firstOrder.ID) + recorder := test.TestEndpoint(http.MethodPost, url, nil, test.Data.testUserToken) + body, err := ioutil.ReadAll(recorder.Body) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, recorder.Code, "Failure: %s", string(body)) + + downloadsAfter := currentDownloads(test) + + assert.Equal(t, len(downloadsBefore)+1, len(downloadsAfter)) + exists := false + for _, download := range downloadsAfter { + found := false + for _, prev := range downloadsBefore { + if download.ID == prev.ID { + found = true + break + } + } + if !found { + assert.Equal(t, "/my/special/new/url", download.URL) + assert.Equal(t, "123-i-can-fly-456", download.Sku) + exists = true + } + } + assert.True(t, exists) +} diff --git a/api/order.go b/api/order.go index b873a88..63654ae 100644 --- a/api/order.go +++ b/api/order.go @@ -7,7 +7,6 @@ import ( "net/http" "sync" - "github.com/PuerkitoBio/goquery" "github.com/go-chi/chi" "github.com/jinzhu/gorm" "github.com/mattes/vat" @@ -641,6 +640,13 @@ func (a *API) createLineItems(ctx context.Context, tx *gorm.DB, order *models.Or Path: orderItem.Path, OrderID: order.ID, } + + for _, addon := range orderItem.Addons { + lineItem.AddonItems = append(lineItem.AddonItems, &models.AddonItem{ + Sku: addon.Sku, + }) + } + order.LineItems = append(order.LineItems, lineItem) sem <- 1 wg.Add(1) @@ -654,7 +660,7 @@ func (a *API) createLineItems(ctx context.Context, tx *gorm.DB, order *models.Or return } - if err := a.processLineItem(ctx, order, item, orderItem); err != nil { + if err := a.processLineItem(ctx, order, item); err != nil { sharedErr.setError(err) } }(lineItem, orderItem) @@ -735,56 +741,11 @@ func (a *API) processAddress(tx *gorm.DB, order *models.Order, name string, addr return address, nil } -func (a *API) processLineItem(ctx context.Context, order *models.Order, item *models.LineItem, orderItem *orderLineItem) error { +func (a *API) processLineItem(ctx context.Context, order *models.Order, item *models.LineItem) error { config := gcontext.GetConfig(ctx) jwtClaims := gcontext.GetClaimsAsMap(ctx) - resp, err := a.httpClient.Get(config.SiteURL + item.Path) - if err != nil { - return err - } - defer resp.Body.Close() - - doc, err := goquery.NewDocumentFromResponse(resp) - if err != nil { - return err - } - - metaTag := doc.Find(".gocommerce-product") - if metaTag.Length() == 0 { - return fmt.Errorf("No script tag with class gocommerce-product tag found for '%v'", item.Title) - } - metaProducts := []*models.LineItemMetadata{} - var parsingErr error - metaTag.EachWithBreak(func(_ int, tag *goquery.Selection) bool { - meta := &models.LineItemMetadata{} - parsingErr = json.Unmarshal([]byte(tag.Text()), meta) - if parsingErr != nil { - return false - } - metaProducts = append(metaProducts, meta) - return true - }) - if parsingErr != nil { - return fmt.Errorf("Error parsing product metadata: %v", parsingErr) - } - - if len(metaProducts) == 1 && item.Sku == "" { - item.Sku = metaProducts[0].Sku - } - - for _, meta := range metaProducts { - if meta.Sku == item.Sku { - for _, addon := range orderItem.Addons { - item.AddonItems = append(item.AddonItems, &models.AddonItem{ - Sku: addon.Sku, - }) - } - - return item.Process(jwtClaims, order, meta) - } - } - return fmt.Errorf("No product Sku from path matched: %v", item.Sku) + return item.Process(config, jwtClaims, order) } func orderQuery(db *gorm.DB) *gorm.DB { diff --git a/api/utils_test.go b/api/utils_test.go index 55f0681..1b68d06 100644 --- a/api/utils_test.go +++ b/api/utils_test.go @@ -418,35 +418,34 @@ func signInstanceRequest(req *http.Request, instanceID string, jwtSecret string) // TEST SITE // ------------------------------------------------------------------------------------------------ +func productMetaFrame(meta string) string { + return fmt.Sprintf(` + +