From 6589416c8869622dccba328ab2d5ce04b0324b78 Mon Sep 17 00:00:00 2001 From: highesttt Date: Mon, 2 Nov 2026 23:44:04 -0500 Subject: [PATCH 1/2] page owned sticker packs without aborting on offset mismatch --- pkg/line/stickers.go | 19 +++-------- pkg/line/stickers_test.go | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 pkg/line/stickers_test.go diff --git a/pkg/line/stickers.go b/pkg/line/stickers.go index 9de345e..11f0446 100644 --- a/pkg/line/stickers.go +++ b/pkg/line/stickers.go @@ -28,18 +28,17 @@ 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 } var result struct { Code int `json:"code"` Message string `json:"message"` - Data *struct { + Data struct { Products []ShopProduct `json:"productList"` - Offset int `json:"offset"` Total int `json:"totalSize"` } `json:"data"` } @@ -49,18 +48,10 @@ 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) } - 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 { + products = append(products, result.Data.Products...) + if len(result.Data.Products) == 0 || len(products) >= result.Data.Total { 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..7aca753 --- /dev/null +++ b/pkg/line/stickers_test.go @@ -0,0 +1,67 @@ +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, ",")) + } + tests := []struct { + name string + pages []string + wantIDs []string + wantOffsets []int + }{ + {"ignores echoed offset and short pages", []string{page(3, "1", "2"), page(3, "3")}, []string{"1", "2", "3"}, []int{0, 2}}, + {"stops on empty page", []string{page(5, "1", "2"), page(5)}, []string{"1", "2"}, []int{0, 2}}, + {"missing total", []string{page(-1, "1")}, []string{"1"}, []int{0}}, + } + 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 { + t.Fatal(err) + } + 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) + } + }) + } +} From 0e2f623ca106049d3b1596c260bd0b974e0f0b0a Mon Sep 17 00:00:00 2001 From: highesttt Date: Tue, 3 Nov 2026 00:35:31 -0500 Subject: [PATCH 2/2] stickers: error on null data and early empty pages --- pkg/line/stickers.go | 15 ++++++++++++--- pkg/line/stickers_test.go | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pkg/line/stickers.go b/pkg/line/stickers.go index 11f0446..a0abdf1 100644 --- a/pkg/line/stickers.go +++ b/pkg/line/stickers.go @@ -37,7 +37,7 @@ func (c *Client) GetOwnedStickerProducts(ctx context.Context, country string) ([ var result struct { Code int `json:"code"` Message string `json:"message"` - Data struct { + Data *struct { Products []ShopProduct `json:"productList"` Total int `json:"totalSize"` } `json:"data"` @@ -47,9 +47,18 @@ 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") } - products = append(products, result.Data.Products...) - if len(result.Data.Products) == 0 || len(products) >= result.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 } } diff --git a/pkg/line/stickers_test.go b/pkg/line/stickers_test.go index 7aca753..e0ce2f9 100644 --- a/pkg/line/stickers_test.go +++ b/pkg/line/stickers_test.go @@ -23,15 +23,24 @@ func TestGetOwnedStickerProductsPaging(t *testing.T) { } 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}}, - {"stops on empty page", []string{page(5, "1", "2"), page(5)}, []string{"1", "2"}, []int{0, 2}}, - {"missing total", []string{page(-1, "1")}, []string{"1"}, []int{0}}, + {"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) { @@ -52,8 +61,8 @@ func TestGetOwnedStickerProductsPaging(t *testing.T) { return obsResponse(http.StatusOK, tt.pages[len(offsets)-1]), nil })} products, err := client.GetOwnedStickerProducts(context.Background(), "JP") - if err != nil { - t.Fatal(err) + if (err != nil) != tt.wantErr { + t.Fatalf("err = %v, wantErr %v", err, tt.wantErr) } var ids []string for _, product := range products {