From e908531befa13b53bba81bb9fb4f436727c841ef Mon Sep 17 00:00:00 2001 From: Pierig Le Saux Date: Sat, 11 Apr 2026 00:09:08 -0400 Subject: [PATCH 1/3] feat(api): add NumPackages to mirrors/repos/snapshots list responses add API response wrappers with NumPackages derived from RefList length; keep show endpoint payloads unchanged for backward compatibility; add API tests for list endpoint NumPackages; update swagger response schemas for list endpoints --- api/mirror.go | 18 ++++++++++---- api/mirror_test.go | 37 +++++++++++++++++++++++++++- api/package_count_response.go | 30 +++++++++++++++++++++++ api/repos.go | 17 +++++++++---- api/repos_test.go | 45 +++++++++++++++++++++++++++++++++++ api/snapshot.go | 17 +++++++++---- api/snapshot_test.go | 45 +++++++++++++++++++++++++++++++++++ 7 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 api/package_count_response.go create mode 100644 api/repos_test.go create mode 100644 api/snapshot_test.go diff --git a/api/mirror.go b/api/mirror.go index aa4af0a2..bf8d3bad 100644 --- a/api/mirror.go +++ b/api/mirror.go @@ -66,17 +66,26 @@ func uniqueStrings(input []string) []string { // @Description Each mirror is returned as in “show” API. // @Tags Mirrors // @Produce json -// @Success 200 {array} deb.RemoteRepo +// @Success 200 {array} remoteRepoResponse // @Router /api/mirrors [get] func apiMirrorsList(c *gin.Context) { collectionFactory := context.NewCollectionFactory() collection := collectionFactory.RemoteRepoCollection() - result := []*deb.RemoteRepo{} - _ = collection.ForEach(func(repo *deb.RemoteRepo) error { - result = append(result, repo) + result := []remoteRepoResponse{} + err := collection.ForEach(func(repo *deb.RemoteRepo) error { + err := collection.LoadComplete(repo) + if err != nil { + return err + } + + result = append(result, newRemoteRepoResponse(repo)) return nil }) + if err != nil { + AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err)) + return + } c.JSON(200, result) } @@ -264,6 +273,7 @@ func apiMirrorsShow(c *gin.Context) { err = collection.LoadComplete(repo) if err != nil { AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err)) + return } c.JSON(200, repo) diff --git a/api/mirror_test.go b/api/mirror_test.go index cb64d485..6f2c04c0 100644 --- a/api/mirror_test.go +++ b/api/mirror_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" + "github.com/aptly-dev/aptly/deb" "github.com/gin-gonic/gin" . "gopkg.in/check.v1" ) @@ -17,7 +18,10 @@ var _ = Suite(&MirrorSuite{}) func (s *MirrorSuite) TestGetMirrors(c *C) { response, _ := s.HTTPRequest("GET", "/api/mirrors", nil) c.Check(response.Code, Equals, 200) - c.Check(response.Body.String(), Equals, "[]") + + var mirrors []map[string]interface{} + err := json.Unmarshal(response.Body.Bytes(), &mirrors) + c.Assert(err, IsNil) } func (s *MirrorSuite) TestDeleteMirrorNonExisting(c *C) { @@ -53,3 +57,34 @@ func (s *MirrorSuite) TestCreateMirror(c *C) { c.Check(response.Code, Equals, 400) c.Check(response.Body.String(), Equals, "") } + +func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) { + collection := s.context.NewCollectionFactory().RemoteRepoCollection() + + repo, err := deb.NewRemoteRepo("count-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false) + c.Assert(err, IsNil) + + err = collection.Add(repo) + c.Assert(err, IsNil) + + response, err := s.HTTPRequest("GET", "/api/mirrors", nil) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 200) + + var mirrors []map[string]interface{} + err = json.Unmarshal(response.Body.Bytes(), &mirrors) + c.Assert(err, IsNil) + + found := false + for _, mirror := range mirrors { + if mirror["Name"] == "count-mirror" { + found = true + value, ok := mirror["NumPackages"] + c.Assert(ok, Equals, true) + c.Assert(value, Equals, float64(0)) + break + } + } + + c.Assert(found, Equals, true) +} diff --git a/api/package_count_response.go b/api/package_count_response.go new file mode 100644 index 00000000..4edb215d --- /dev/null +++ b/api/package_count_response.go @@ -0,0 +1,30 @@ +package api + +import "github.com/aptly-dev/aptly/deb" + +type remoteRepoResponse struct { + *deb.RemoteRepo + NumPackages int `json:"NumPackages"` +} + +type localRepoResponse struct { + *deb.LocalRepo + NumPackages int `json:"NumPackages"` +} + +type snapshotResponse struct { + *deb.Snapshot + NumPackages int `json:"NumPackages"` +} + +func newRemoteRepoResponse(repo *deb.RemoteRepo) remoteRepoResponse { + return remoteRepoResponse{RemoteRepo: repo, NumPackages: repo.NumPackages()} +} + +func newLocalRepoResponse(repo *deb.LocalRepo) localRepoResponse { + return localRepoResponse{LocalRepo: repo, NumPackages: repo.NumPackages()} +} + +func newSnapshotResponse(snapshot *deb.Snapshot) snapshotResponse { + return snapshotResponse{Snapshot: snapshot, NumPackages: snapshot.NumPackages()} +} diff --git a/api/repos.go b/api/repos.go index 6f418bf8..77dd5172 100644 --- a/api/repos.go +++ b/api/repos.go @@ -69,17 +69,26 @@ func reposServeInAPIMode(c *gin.Context) { // @Description Each repo is returned as in “show” API. // @Tags Repos // @Produce json -// @Success 200 {array} deb.LocalRepo +// @Success 200 {array} localRepoResponse // @Router /api/repos [get] func apiReposList(c *gin.Context) { - result := []*deb.LocalRepo{} + result := []localRepoResponse{} collectionFactory := context.NewCollectionFactory() collection := collectionFactory.LocalRepoCollection() - _ = collection.ForEach(func(r *deb.LocalRepo) error { - result = append(result, r) + err := collection.ForEach(func(r *deb.LocalRepo) error { + err := collection.LoadComplete(r) + if err != nil { + return err + } + + result = append(result, newLocalRepoResponse(r)) return nil }) + if err != nil { + AbortWithJSONError(c, 500, err) + return + } c.JSON(200, result) } diff --git a/api/repos_test.go b/api/repos_test.go new file mode 100644 index 00000000..267221cc --- /dev/null +++ b/api/repos_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "bytes" + "encoding/json" + + "github.com/gin-gonic/gin" + . "gopkg.in/check.v1" +) + +type ReposSuite struct { + APISuite +} + +var _ = Suite(&ReposSuite{}) + +func (s *ReposSuite) TestGetReposIncludesNumPackages(c *C) { + body, err := json.Marshal(gin.H{"Name": "count-repo-list"}) + c.Assert(err, IsNil) + + response, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body)) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 201) + + response, err = s.HTTPRequest("GET", "/api/repos", nil) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 200) + + var repos []map[string]interface{} + err = json.Unmarshal(response.Body.Bytes(), &repos) + c.Assert(err, IsNil) + + found := false + for _, repo := range repos { + if repo["Name"] == "count-repo-list" { + found = true + value, ok := repo["NumPackages"] + c.Assert(ok, Equals, true) + c.Assert(value, Equals, float64(0)) + break + } + } + + c.Assert(found, Equals, true) +} diff --git a/api/snapshot.go b/api/snapshot.go index 24f276c5..2c50566f 100644 --- a/api/snapshot.go +++ b/api/snapshot.go @@ -20,7 +20,7 @@ import ( // @Description Each snapshot is returned as in “show” API. // @Tags Snapshots // @Produce json -// @Success 200 {array} deb.Snapshot +// @Success 200 {array} snapshotResponse // @Router /api/snapshots [get] func apiSnapshotsList(c *gin.Context) { SortMethodString := c.Request.URL.Query().Get("sort") @@ -32,11 +32,20 @@ func apiSnapshotsList(c *gin.Context) { SortMethodString = "name" } - result := []*deb.Snapshot{} - _ = collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error { - result = append(result, snapshot) + result := []snapshotResponse{} + err := collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error { + err := collection.LoadComplete(snapshot) + if err != nil { + return err + } + + result = append(result, newSnapshotResponse(snapshot)) return nil }) + if err != nil { + AbortWithJSONError(c, 500, err) + return + } c.JSON(200, result) } diff --git a/api/snapshot_test.go b/api/snapshot_test.go new file mode 100644 index 00000000..d3ee3535 --- /dev/null +++ b/api/snapshot_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "bytes" + "encoding/json" + + "github.com/gin-gonic/gin" + . "gopkg.in/check.v1" +) + +type SnapshotsSuite struct { + APISuite +} + +var _ = Suite(&SnapshotsSuite{}) + +func (s *SnapshotsSuite) TestGetSnapshotsIncludesNumPackages(c *C) { + body, err := json.Marshal(gin.H{"Name": "count-snapshot-list"}) + c.Assert(err, IsNil) + + response, err := s.HTTPRequest("POST", "/api/snapshots", bytes.NewReader(body)) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 201) + + response, err = s.HTTPRequest("GET", "/api/snapshots", nil) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 200) + + var snapshots []map[string]interface{} + err = json.Unmarshal(response.Body.Bytes(), &snapshots) + c.Assert(err, IsNil) + + found := false + for _, snapshot := range snapshots { + if snapshot["Name"] == "count-snapshot-list" { + found = true + value, ok := snapshot["NumPackages"] + c.Assert(ok, Equals, true) + c.Assert(value, Equals, float64(0)) + break + } + } + + c.Assert(found, Equals, true) +} From 92d7561d49804455edec1678a9305f328c4a3c12 Mon Sep 17 00:00:00 2001 From: Pierig Le Saux Date: Sat, 11 Apr 2026 01:44:19 -0400 Subject: [PATCH 2/3] test(api): add coverage for NumPackages list handlers and error paths --- api/mirror_test.go | 17 ++++++++++++- api/package_count_test_helpers_test.go | 19 ++++++++++++++ api/repos_test.go | 34 ++++++++++++++++++++------ api/snapshot_test.go | 28 +++++++++++++-------- 4 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 api/package_count_test_helpers_test.go diff --git a/api/mirror_test.go b/api/mirror_test.go index 6f2c04c0..16c4a0ef 100644 --- a/api/mirror_test.go +++ b/api/mirror_test.go @@ -66,6 +66,7 @@ func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) { err = collection.Add(repo) c.Assert(err, IsNil) + putRawDBValue(c, &s.APISuite, repo.RefKey(), makePackageRefList(c).Encode()) response, err := s.HTTPRequest("GET", "/api/mirrors", nil) c.Assert(err, IsNil) @@ -81,10 +82,24 @@ func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) { found = true value, ok := mirror["NumPackages"] c.Assert(ok, Equals, true) - c.Assert(value, Equals, float64(0)) + c.Assert(value, Equals, float64(2)) break } } c.Assert(found, Equals, true) } + +func (s *MirrorSuite) TestGetMirrorsReturns500OnCorruptRefList(c *C) { + collection := s.context.NewCollectionFactory().RemoteRepoCollection() + + repo, err := deb.NewRemoteRepo("broken-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false) + c.Assert(err, IsNil) + c.Assert(collection.Add(repo), IsNil) + putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack")) + + response, err := s.HTTPRequest("GET", "/api/mirrors", nil) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 500) + c.Assert(response.Body.String(), Matches, ".*unable to show:.*") +} diff --git a/api/package_count_test_helpers_test.go b/api/package_count_test_helpers_test.go new file mode 100644 index 00000000..fc86ef3c --- /dev/null +++ b/api/package_count_test_helpers_test.go @@ -0,0 +1,19 @@ +package api + +import ( + "github.com/aptly-dev/aptly/deb" + . "gopkg.in/check.v1" +) + +func makePackageRefList(c *C) *deb.PackageRefList { + list := deb.NewPackageList() + c.Assert(list.Add(&deb.Package{Name: "libcount", Version: "1.0", Architecture: "amd64"}), IsNil) + c.Assert(list.Add(&deb.Package{Name: "appcount", Version: "2.0", Architecture: "all"}), IsNil) + return deb.NewPackageRefListFromPackageList(list) +} + +func putRawDBValue(c *C, s *APISuite, key []byte, value []byte) { + db, err := s.context.Database() + c.Assert(err, IsNil) + c.Assert(db.Put(key, value), IsNil) +} \ No newline at end of file diff --git a/api/repos_test.go b/api/repos_test.go index 267221cc..55117316 100644 --- a/api/repos_test.go +++ b/api/repos_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" + "github.com/aptly-dev/aptly/deb" "github.com/gin-gonic/gin" . "gopkg.in/check.v1" ) @@ -15,14 +16,12 @@ type ReposSuite struct { var _ = Suite(&ReposSuite{}) func (s *ReposSuite) TestGetReposIncludesNumPackages(c *C) { - body, err := json.Marshal(gin.H{"Name": "count-repo-list"}) - c.Assert(err, IsNil) + collection := s.context.NewCollectionFactory().LocalRepoCollection() + repo := deb.NewLocalRepo("count-repo-list", "") + repo.UpdateRefList(makePackageRefList(c)) + c.Assert(collection.Add(repo), IsNil) - response, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body)) - c.Assert(err, IsNil) - c.Assert(response.Code, Equals, 201) - - response, err = s.HTTPRequest("GET", "/api/repos", nil) + response, err := s.HTTPRequest("GET", "/api/repos", nil) c.Assert(err, IsNil) c.Assert(response.Code, Equals, 200) @@ -36,10 +35,29 @@ func (s *ReposSuite) TestGetReposIncludesNumPackages(c *C) { found = true value, ok := repo["NumPackages"] c.Assert(ok, Equals, true) - c.Assert(value, Equals, float64(0)) + c.Assert(value, Equals, float64(2)) break } } c.Assert(found, Equals, true) } + +func (s *ReposSuite) TestGetReposReturns500OnCorruptRefList(c *C) { + body, err := json.Marshal(gin.H{"Name": "broken-repo-list"}) + c.Assert(err, IsNil) + + response, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body)) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 201) + + collection := s.context.NewCollectionFactory().LocalRepoCollection() + repo, err := collection.ByName("broken-repo-list") + c.Assert(err, IsNil) + putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack")) + + response, err = s.HTTPRequest("GET", "/api/repos", nil) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 500) + c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*") +} diff --git a/api/snapshot_test.go b/api/snapshot_test.go index d3ee3535..36c50809 100644 --- a/api/snapshot_test.go +++ b/api/snapshot_test.go @@ -1,10 +1,9 @@ package api import ( - "bytes" "encoding/json" - "github.com/gin-gonic/gin" + "github.com/aptly-dev/aptly/deb" . "gopkg.in/check.v1" ) @@ -15,14 +14,11 @@ type SnapshotsSuite struct { var _ = Suite(&SnapshotsSuite{}) func (s *SnapshotsSuite) TestGetSnapshotsIncludesNumPackages(c *C) { - body, err := json.Marshal(gin.H{"Name": "count-snapshot-list"}) - c.Assert(err, IsNil) + collection := s.context.NewCollectionFactory().SnapshotCollection() + snapshot := deb.NewSnapshotFromRefList("count-snapshot-list", nil, makePackageRefList(c), "") + c.Assert(collection.Add(snapshot), IsNil) - response, err := s.HTTPRequest("POST", "/api/snapshots", bytes.NewReader(body)) - c.Assert(err, IsNil) - c.Assert(response.Code, Equals, 201) - - response, err = s.HTTPRequest("GET", "/api/snapshots", nil) + response, err := s.HTTPRequest("GET", "/api/snapshots", nil) c.Assert(err, IsNil) c.Assert(response.Code, Equals, 200) @@ -36,10 +32,22 @@ func (s *SnapshotsSuite) TestGetSnapshotsIncludesNumPackages(c *C) { found = true value, ok := snapshot["NumPackages"] c.Assert(ok, Equals, true) - c.Assert(value, Equals, float64(0)) + c.Assert(value, Equals, float64(2)) break } } c.Assert(found, Equals, true) } + +func (s *SnapshotsSuite) TestGetSnapshotsReturns500OnCorruptRefList(c *C) { + collection := s.context.NewCollectionFactory().SnapshotCollection() + snapshot := deb.NewSnapshotFromRefList("broken-snapshot-list", nil, makePackageRefList(c), "") + c.Assert(collection.Add(snapshot), IsNil) + putRawDBValue(c, &s.APISuite, snapshot.RefKey(), []byte("not-msgpack")) + + response, err := s.HTTPRequest("GET", "/api/snapshots", nil) + c.Assert(err, IsNil) + c.Assert(response.Code, Equals, 500) + c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*") +} From 0b84009b4aaacb89d902d206821748b3b00331f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Roth?= Date: Sun, 26 Apr 2026 17:53:18 +0200 Subject: [PATCH 3/3] tests: add new arguments --- api/mirror_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/mirror_test.go b/api/mirror_test.go index 16c4a0ef..65433e7f 100644 --- a/api/mirror_test.go +++ b/api/mirror_test.go @@ -61,7 +61,7 @@ func (s *MirrorSuite) TestCreateMirror(c *C) { func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) { collection := s.context.NewCollectionFactory().RemoteRepoCollection() - repo, err := deb.NewRemoteRepo("count-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false) + repo, err := deb.NewRemoteRepo("count-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false) c.Assert(err, IsNil) err = collection.Add(repo) @@ -93,7 +93,7 @@ func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) { func (s *MirrorSuite) TestGetMirrorsReturns500OnCorruptRefList(c *C) { collection := s.context.NewCollectionFactory().RemoteRepoCollection() - repo, err := deb.NewRemoteRepo("broken-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false) + repo, err := deb.NewRemoteRepo("broken-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false) c.Assert(err, IsNil) c.Assert(collection.Add(repo), IsNil) putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack"))