Skip to content

Commit 6faa7be

Browse files
JAORMXclaude
andcommitted
Return 404 for unknown group in workload listing
FilterByGroup returns an empty slice (nil error) for a group that does not exist, so a typo'd ?group= silently returned 200 with an empty list instead of the documented 404. Check group existence explicitly via the group manager before filtering, in both the upgrade-check and the listWorkloads handlers, so the advertised 404 is real. Add a bulk upgrade-check test covering the unknown-group path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4adf15c commit 6faa7be

3 files changed

Lines changed: 53 additions & 2 deletions

File tree

pkg/api/v1/workloads.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,18 @@ func (s *WorkloadRoutes) listWorkloads(w http.ResponseWriter, r *http.Request) e
144144
http.StatusBadRequest,
145145
)
146146
}
147+
// FilterByGroup silently returns an empty slice for an unknown group,
148+
// so check existence explicitly to honor the documented 404.
149+
exists, existsErr := s.groupManager.Exists(ctx, groupFilter)
150+
if existsErr != nil {
151+
return fmt.Errorf("failed to check group existence: %w", existsErr)
152+
}
153+
if !exists {
154+
return fmt.Errorf("%w: %s", groups.ErrGroupNotFound, groupFilter)
155+
}
147156
workloadList, err = workloads.FilterByGroup(workloadList, groupFilter)
148157
if err != nil {
149-
return err // groups.ErrGroupNotFound already has 404 status code
158+
return err
150159
}
151160
}
152161

pkg/api/v1/workloads_upgrade.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/stacklok/toolhive-core/httperr"
1616
groupval "github.com/stacklok/toolhive-core/validation/group"
17+
"github.com/stacklok/toolhive/pkg/groups"
1718
"github.com/stacklok/toolhive/pkg/registry"
1819
"github.com/stacklok/toolhive/pkg/runner"
1920
"github.com/stacklok/toolhive/pkg/state"
@@ -120,9 +121,18 @@ func (s *WorkloadRoutes) upgradeCheckBulk(w http.ResponseWriter, r *http.Request
120121
http.StatusBadRequest,
121122
)
122123
}
124+
// FilterByGroup silently returns an empty slice for an unknown group,
125+
// so check existence explicitly to honor the documented 404.
126+
exists, existsErr := s.groupManager.Exists(ctx, groupFilter)
127+
if existsErr != nil {
128+
return fmt.Errorf("failed to check group existence: %w", existsErr)
129+
}
130+
if !exists {
131+
return fmt.Errorf("%w: %s", groups.ErrGroupNotFound, groupFilter)
132+
}
123133
workloadList, err = workloads.FilterByGroup(workloadList, groupFilter)
124134
if err != nil {
125-
return err // groups.ErrGroupNotFound already has 404 status code
135+
return err
126136
}
127137
}
128138

pkg/api/v1/workloads_upgrade_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
apierrors "github.com/stacklok/toolhive/pkg/api/errors"
2121
"github.com/stacklok/toolhive/pkg/container/runtime"
2222
"github.com/stacklok/toolhive/pkg/core"
23+
groupsmocks "github.com/stacklok/toolhive/pkg/groups/mocks"
2324
"github.com/stacklok/toolhive/pkg/registry"
2425
registrymocks "github.com/stacklok/toolhive/pkg/registry/mocks"
2526
"github.com/stacklok/toolhive/pkg/runner"
@@ -225,11 +226,15 @@ func TestUpgradeCheckBulk(t *testing.T) {
225226
}, nil)
226227
provider.EXPECT().GetServer("io.github/fetch").Return(imageServer("ghcr.io/example/fetch:1.1.0"), nil)
227228

229+
gm := groupsmocks.NewMockManager(ctrl)
230+
gm.EXPECT().Exists(gomock.Any(), "prod").Return(true, nil)
231+
228232
configs := map[string]*runner.RunConfig{
229233
"fetch": {Name: "fetch", Image: "ghcr.io/example/fetch:1.0.0", RegistryServerName: "io.github/fetch"},
230234
"time": {Name: "time", Image: "ghcr.io/example/time:1.0.0", RegistryServerName: "io.github/time"},
231235
}
232236
routes := upgradeTestRoutes(wm, provider, configs)
237+
routes.groupManager = gm
233238

234239
req := httptest.NewRequest("GET", "/upgrade-check?group=prod", nil)
235240
w := httptest.NewRecorder()
@@ -244,6 +249,33 @@ func TestUpgradeCheckBulk(t *testing.T) {
244249
assert.Equal(t, upgrade.StatusUpgradeAvailable, resp.Results[0].Status)
245250
})
246251

252+
t.Run("unknown group returns 404", func(t *testing.T) {
253+
t.Parallel()
254+
255+
ctrl := gomock.NewController(t)
256+
defer ctrl.Finish()
257+
258+
wm := workloadsmocks.NewMockManager(ctrl)
259+
provider := registrymocks.NewMockProvider(ctrl)
260+
261+
// The group does not exist, so no per-workload registry lookup happens.
262+
wm.EXPECT().ListWorkloads(gomock.Any(), false).Return([]core.Workload{
263+
{Name: "fetch", Group: "prod"},
264+
}, nil)
265+
266+
gm := groupsmocks.NewMockManager(ctrl)
267+
gm.EXPECT().Exists(gomock.Any(), "ghost").Return(false, nil)
268+
269+
routes := upgradeTestRoutes(wm, provider, map[string]*runner.RunConfig{})
270+
routes.groupManager = gm
271+
272+
req := httptest.NewRequest("GET", "/upgrade-check?group=ghost", nil)
273+
w := httptest.NewRecorder()
274+
apierrors.ErrorHandler(routes.upgradeCheckBulk).ServeHTTP(w, req)
275+
276+
require.Equal(t, http.StatusNotFound, w.Code)
277+
})
278+
247279
t.Run("invalid group name", func(t *testing.T) {
248280
t.Parallel()
249281

0 commit comments

Comments
 (0)