From a6b9378fd36742006fef63ca8967ef779a42a384 Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Sat, 22 Aug 2026 08:50:03 +0000 Subject: [PATCH] fix(pins): send explicit match=exact on pins_list name filter pins_list's exact name filter was sent as a bare `name` with no `match` strategy, so pinning-service backends that require an explicit strategy ignored it and returned the full list. status/limit kept working (they are different params) and search kept working (WithFilterNamePartial pins match=partial), which is why only the name filter appeared broken. Send WithFilterMatch(ipfs.MatchExact) alongside the name, mirroring the partial-match search path, and add a guard test asserting match=exact is sent (symmetric with the existing match=partial guard). --- internal/cli/pinning_client.go | 8 +++-- internal/cli/pinning_service_test.go | 53 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/internal/cli/pinning_client.go b/internal/cli/pinning_client.go index 6a80c0e1..e523dd20 100644 --- a/internal/cli/pinning_client.go +++ b/internal/cli/pinning_client.go @@ -242,8 +242,12 @@ func (s *PinningServiceDefault) listViaSDK(ctx context.Context, nameFilter strin // strategy type stays encapsulated. opts = append(opts, ipfs.WithFilterNamePartial(search)) } else if nameFilter != "" { - // Exact name match (default match strategy per spec). - opts = append(opts, ipfs.WithFilterName(nameFilter)) + // Exact name match, sent with an explicit match=exact strategy. The + // suffix-search path above pins match=partial; without a declared match + // the name would ride bare and pinning-service backends that require an + // explicit strategy would ignore it (returning the full list) even + // though status/limit still filter. + opts = append(opts, ipfs.WithFilterName(nameFilter), ipfs.WithFilterMatch(ipfs.MatchExact)) } if statusFilter != "" { opts = append(opts, ipfs.WithFilterStatus(ipfs.PinStatusEnum(statusFilter))) diff --git a/internal/cli/pinning_service_test.go b/internal/cli/pinning_service_test.go index 72de431a..25eff763 100644 --- a/internal/cli/pinning_service_test.go +++ b/internal/cli/pinning_service_test.go @@ -1073,3 +1073,56 @@ func TestPinsListSendsPartialMatch(t *testing.T) { require.Len(t, pins, 1) assert.Equal(t, "do", pins[0].Name) } + +// TestPinsListSendsExactNameMatch is the server-side guard for pins_list's +// exact-name filter: when List receives a non-empty nameFilter (and no search) +// it must reach the ipfs-sdk pinning service with an explicit match=exact name +// filter. A bare name with no declared match strategy is ignored by pinning +// backends that require one, which made the filter a silent no-op (full list +// returned) even though status/limit still filtered. +func TestPinsListSendsExactNameMatch(t *testing.T) { + cfgMgr := configmocks.NewMockManager(t) + cfgMgr.EXPECT().Config().Maybe().Return(&config.Config{AuthToken: testAuthToken}) + + boxoClient := climocks.NewMockPinningClient(t) + boxoClient.EXPECT().LsSync(mock.Anything, mock.Anything, mock.Anything).Maybe() + + sdkSvc := servicemocks.NewMockPinningService(t) + sdkSvc.EXPECT().ListPins(mock.Anything, mock.Anything, mock.Anything). + Return([]ipfs.PinStatus{ + {Pin: ipfs.Pin{Cid: "QmYyy", Name: ptrStr("audit-round4-url")}, PinStatusEnum: ipfs.StatusPinned, Created: time.Now()}, + }, nil). + Run(func(_ctx context.Context, opts ...ipfs.ListOption) { + var ( + name *string + match *ipfs.TextMatchingStrategy + ) + for _, o := range opts { + if o.Name != nil { + n := string(*o.Name) + name = &n + } + if o.Match != nil { + m := ipfs.TextMatchingStrategy(*o.Match) + match = &m + } + } + if name == nil || *name != "audit-round4-url" { + t.Errorf("ListPins must receive WithFilterName(%q), got name=%v", "audit-round4-url", name) + } + if match == nil || *match != ipfs.MatchExact { + t.Errorf("ListPins must receive match=exact for exact name filter, got match=%v", match) + } + }) + + output := newTestOutput() + service := NewPinningService(cfgMgr, output, "https://api.test.com", + WithPinningClient(boxoClient), + WithSDKPinningService(sdkSvc), + ) + + pins, err := service.List(context.Background(), "audit-round4-url", 0, "", "") + require.NoError(t, err) + require.Len(t, pins, 1) + assert.Equal(t, "audit-round4-url", pins[0].Name) +}