package shopify import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func TestShopifySKUSearchQueryOR(t *testing.T) { got := shopifySKUSearchQueryOR([]string{"A", "B\"C", ""}) want := `sku:"A" OR sku:"B\"C"` if got != want { t.Fatalf("got %q want %q", got, want) } } func TestFindProductsBySKUsBatchesOneGraphQLCall(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/admin/api/2024-10/graphql.json" { t.Fatalf("unexpected path %s", r.URL.Path) } calls.Add(1) var body struct { Variables struct { Q string `json:"q"` N int `json:"n"` } `json:"variables"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatal(err) } if body.Variables.N != 3 { t.Fatalf("expected n=3 got %d", body.Variables.N) } if !strings.Contains(body.Variables.Q, `sku:"SKU-1"`) || !strings.Contains(body.Variables.Q, `sku:"SKU-2"`) { t.Fatalf("query missing SKUs: %q", body.Variables.Q) } _, _ = io.WriteString(w, `{ "data": { "productVariants": { "edges": [ {"node":{"sku":"SKU-1","product":{"id":"gid://shopify/Product/101","title":"One"}}}, {"node":{"sku":"SKU-2","product":{"id":"gid://shopify/Product/102","title":"Two"}}} ] } } }`) })) defer srv.Close() c := NewClient("demo.myshopify.com", "tok", "2024-10", srv.Client()) c.HTTP = rewriteShopifyHost(srv, c.HTTP) found, err := c.FindProductsBySKUs(t.Context(), []string{"SKU-1", "SKU-2", "SKU-MISSING"}) if err != nil { t.Fatal(err) } if calls.Load() != 1 { t.Fatalf("expected 1 GraphQL call, got %d", calls.Load()) } if found["SKU-1"].ID != 101 || found["SKU-2"].ID != 102 { t.Fatalf("unexpected map: %+v", found) } if _, ok := found["SKU-MISSING"]; ok { t.Fatal("missing SKU should be absent") } } func TestShopifyRESTRetries429(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n := calls.Add(1) if n == 1 { w.Header().Set("Retry-After", "0") w.WriteHeader(http.StatusTooManyRequests) _, _ = io.WriteString(w, `{"errors":"throttle"}`) return } _, _ = io.WriteString(w, `{"shop":{"id":1,"name":"Demo","domain":"demo.myshopify.com","currency":"USD"}}`) })) defer srv.Close() c := NewClient("demo.myshopify.com", "tok", "2024-10", srv.Client()) c.HTTP = rewriteShopifyHost(srv, c.HTTP) start := time.Now() shop, err := c.TestConnection(t.Context()) if err != nil { t.Fatal(err) } if shop == nil || shop.Name != "Demo" { t.Fatalf("unexpected shop %+v", shop) } if calls.Load() != 2 { t.Fatalf("expected 2 attempts, got %d", calls.Load()) } if time.Since(start) > 3*time.Second { t.Fatal("retry waited too long") } } // rewriteShopifyHost routes myshopify Admin API calls to the test server. func rewriteShopifyHost(srv *httptest.Server, base *http.Client) *http.Client { rt := base.Transport if rt == nil { rt = http.DefaultTransport } return &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { u := *req.URL su, _ := http.NewRequest(req.Method, srv.URL+u.Path, req.Body) su.URL.RawQuery = u.RawQuery su.Header = req.Header.Clone() su = su.WithContext(req.Context()) return rt.RoundTrip(su) }), Timeout: base.Timeout, } } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }