diff --git a/pkg/line/stickers.go b/pkg/line/stickers.go index 9de345e..a0abdf1 100644 --- a/pkg/line/stickers.go +++ b/pkg/line/stickers.go @@ -28,9 +28,9 @@ type StickerSummary struct { func (c *Client) GetOwnedStickerProducts(ctx context.Context, country string) ([]ShopProduct, error) { const pageSize, maxProducts = 1000, 100000 var products []ShopProduct - for offset := 0; offset < maxProducts; { + for len(products) < maxProducts { resp, err := c.callRPCWithBaseURLContext(ctx, ShopBaseURL, "ShopService", "getOwnedProductSummaries", - "stickershop", offset, pageSize, map[string]string{"language": "en", "country": country}) + "stickershop", len(products), pageSize, map[string]string{"language": "en", "country": country}) if err != nil { return nil, err } @@ -39,7 +39,6 @@ func (c *Client) GetOwnedStickerProducts(ctx context.Context, country string) ([ Message string `json:"message"` Data *struct { Products []ShopProduct `json:"productList"` - Offset int `json:"offset"` Total int `json:"totalSize"` } `json:"data"` } @@ -48,19 +47,20 @@ func (c *Client) GetOwnedStickerProducts(ctx context.Context, country string) ([ } if result.Code != 0 { return nil, fmt.Errorf("getOwnedProductSummaries failed: code %d: %s", result.Code, result.Message) + } else if result.Data == nil { + return nil, fmt.Errorf("getOwnedProductSummaries returned no data") } - data := result.Data - if data == nil || data.Offset != offset || data.Total < offset || data.Total > maxProducts { - return nil, fmt.Errorf("invalid LINE shop pagination") - } - products = append(products, data.Products...) - offset += len(data.Products) - if offset >= data.Total { + page := result.Data.Products + products = append(products, page...) + if result.Data.Total > 0 { + if len(products) >= result.Data.Total { + return products, nil + } else if len(page) == 0 { + return nil, fmt.Errorf("getOwnedProductSummaries returned empty page at %d of %d products", len(products), result.Data.Total) + } + } else if len(page) < pageSize { return products, nil } - if len(data.Products) == 0 { - return nil, fmt.Errorf("empty LINE shop page before end of catalog") - } } return nil, fmt.Errorf("LINE shop catalog exceeded pagination limit") } diff --git a/pkg/line/stickers_test.go b/pkg/line/stickers_test.go new file mode 100644 index 0000000..e0ce2f9 --- /dev/null +++ b/pkg/line/stickers_test.go @@ -0,0 +1,76 @@ +package line + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "slices" + "strings" + "testing" +) + +func TestGetOwnedStickerProductsPaging(t *testing.T) { + page := func(total int, ids ...string) string { + products := make([]string, len(ids)) + for i, id := range ids { + products[i] = fmt.Sprintf(`{"id":%q}`, id) + } + totalField := "" + if total >= 0 { + totalField = fmt.Sprintf(`,"totalSize":%d`, total) + } + return fmt.Sprintf(`{"code":0,"data":{"offset":0%s,"productList":[%s]}}`, totalField, strings.Join(products, ",")) + } + fullPage := make([]string, 1000) + for i := range fullPage { + fullPage[i] = fmt.Sprint(i) + } + tests := []struct { + name string + pages []string + wantIDs []string + wantOffsets []int + wantErr bool + }{ + {"ignores echoed offset and short pages", []string{page(3, "1", "2"), page(3, "3")}, []string{"1", "2", "3"}, []int{0, 2}, false}, + {"errors on empty page before total", []string{page(5, "1", "2"), page(5)}, nil, []int{0, 2}, true}, + {"errors on null data", []string{`{"code":0,"data":null}`}, nil, []int{0}, true}, + {"missing total", []string{page(-1, "1")}, []string{"1"}, []int{0}, false}, + {"zero total stops on empty page", []string{page(0)}, nil, []int{0}, false}, + {"missing total pages until short page", []string{page(-1, fullPage...), page(-1, "1000")}, append(slices.Clone(fullPage), "1000"), []int{0, 1000}, false}, + {"zero total pages until empty page", []string{page(0, fullPage...), page(0)}, fullPage, []int{0, 1000}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var offsets []int + client := NewClient("token") + client.HTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + var args []json.RawMessage + body, _ := io.ReadAll(req.Body) + if err := json.Unmarshal(body, &args); err != nil || len(args) != 4 || string(args[0]) != `"stickershop"` { + t.Fatalf("unexpected request body %s", body) + } + var offset int + _ = json.Unmarshal(args[1], &offset) + offsets = append(offsets, offset) + if len(offsets) > len(tt.pages) { + t.Fatalf("too many requests") + } + return obsResponse(http.StatusOK, tt.pages[len(offsets)-1]), nil + })} + products, err := client.GetOwnedStickerProducts(context.Background(), "JP") + if (err != nil) != tt.wantErr { + t.Fatalf("err = %v, wantErr %v", err, tt.wantErr) + } + var ids []string + for _, product := range products { + ids = append(ids, product.ID) + } + if !slices.Equal(ids, tt.wantIDs) || !slices.Equal(offsets, tt.wantOffsets) { + t.Fatalf("ids = %v offsets = %v, want %v %v", ids, offsets, tt.wantIDs, tt.wantOffsets) + } + }) + } +}