Files
netbird/management/server/api/v1alpha1/peers/peers_handler_pagination_test.go
T
dmitri-netbird 8fef487e42 [management] Add support for pagination in GetAllPeers call (#8190)
* use libopenapi OpenAPI generator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added a handler for v1alpha1/peers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up request validator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wired up spec-based validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use sync validation; set base url to v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moved stuff around, extracted runtime libopenapu deps into runtime_tooling

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* testing /peers path params

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added user schemas and paths

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use api validation in tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* post-merge fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* deleted tmp command used to test validator integration

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* require request body in post/put requests in order to force validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* making linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* switch to api/v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* go mod tidy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* keep the original order of middleware: metrics, cors, then auth

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* return 422 on validation errors

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* v1alpha1 spec cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* no need for a double-pointer

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more linter fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* removed more double pointers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use di to inject api_v0 and api_v1 http routers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* do not re-add middleware on repeated call to ApiHandler

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for pagination and filtering in /peers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* calling ApiRouter() now also calls ApiV1Router()

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* adding peer filters

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* introduced paginated GetAllPeers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up pagination

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* convert pagination state to query scope

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* peer filter wip

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* implemented peer filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* test for filtering by os

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for sorting of peers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for ordering + tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up total number of records in paginated GetAllPeers call

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleaned up GetAllPeers response

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* pacify linter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleanups and fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moar cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* pacify linter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* remove duplicated test

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* make linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moving GetAllPeersPaginated to peer manager, p. 1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moving GetAllPeersPaginated to peer manager, p. 2

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added peer manager tests for GetPeersPaginated

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* order peers in GetAccountPeersPaginated by their ids by default

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* always order by peer_id, filter out proxies

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed broken tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* support for filtering of peers by peer_id in GetAllPeers()

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* filtering by peer_ids

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use lowercase in 'like' queries, drop ordering by ip

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* drop sort_by ip key

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* drop forced_approve filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* replace mac with darwin in peer os filter

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fix peers query parameter: use peer_ids

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed broken peer_test

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* responded to feedback

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-10-19 22:19:39 +02:00

192 lines
7.0 KiB
Go

package peers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
peersmodule "github.com/netbirdio/netbird/management/internals/modules/peers"
nbcontext "github.com/netbirdio/netbird/management/server/context"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/shared/auth"
"github.com/netbirdio/netbird/shared/management/http/apiv1alpha1"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func TestGetPeersPagination(t *testing.T) {
peer := &nbpeer.Peer{
ID: testPeerID,
Status: &nbpeer.PeerStatus{Connected: true},
Meta: nbpeer.PeerSystemMeta{},
}
tt := []struct {
name string
expectedStatus int
requestPath string
setExpectations func(p *Handler)
}{
{
name: "GetPeers default paging, filtering, and sorting",
requestPath: "/peers",
expectedStatus: http.StatusOK,
setExpectations: func(p *Handler) {
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}), gomock.Any(), gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
},
},
{
name: "GetPeers paging",
requestPath: "/peers?page=10&page_size=250",
expectedStatus: http.StatusOK,
setExpectations: func(p *Handler) {
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Eq(store.PaginationState{Page: 10, PageSize: 250}),
gomock.Any(), gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
},
},
{
name: "GetPeers paging with invalid page",
requestPath: "/peers?page=aaa&page_size=250",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers paging with invalid page size",
requestPath: "/peers?page=10&page_size=aaa",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers filtering all valid params",
requestPath: "/peers?user_id=test-user&group_ids=1&group_ids=2&group_ids=3&group_ids=4&peer_ids=peer-1&peer_ids=peer-2&connected=true&approval_required=false&os=linux&os=windows&os=darwin&os=android&os=ios&os=js&ip=10.10.10.10&mac=00:11:22:33:44:55&name=test&hostname=test-host&kind=user_device",
expectedStatus: http.StatusOK,
setExpectations: func(p *Handler) {
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}),
gomock.Eq(store.PeerFilters{
UserId: "test-user",
GroupIds: []string{"1", "2", "3", "4"},
Connected: new(true),
ApprovalRequired: new(false),
Os: []string{"linux", "windows", "darwin", "android", "ios", "js"},
IP: "10.10.10.10",
MAC: "00:11:22:33:44:55",
Name: "test",
Hostname: "test-host",
IsServer: new(false),
PeersIds: []string{"peer-1", "peer-2"},
}),
gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
},
},
{
name: "GetPeers filtering with kind server",
requestPath: "/peers?kind=server",
expectedStatus: http.StatusOK,
setExpectations: func(p *Handler) {
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}),
gomock.Eq(store.PeerFilters{IsServer: new(true)}),
gomock.Any()).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
},
},
{
name: "GetPeers filtering with invalid connected value",
requestPath: "/peers?connected=aaa",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers filtering with invalid approval_required value",
requestPath: "/peers?approval_required=aaa",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers filtering with invalid os value",
requestPath: "/peers?os=nosuchos",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers filtering with invalid kind value",
requestPath: "/peers?kind=nosuchkind",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers filtering with conflicting user_id and kind",
requestPath: "/peers?user_id=test-user&kind=server",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers sorting preserves field ordering",
requestPath: "/peers?sort_by=last_seen&sort_by=name&sort_by=version",
expectedStatus: http.StatusOK,
setExpectations: func(p *Handler) {
p.peersManager.(*peersmodule.MockManager).EXPECT().GetPeersPaginated(gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Eq(store.PaginationState{Page: 1, PageSize: 100}),
gomock.Any(),
gomock.Eq(store.PeerSorting{
Fields: []store.PeerSortingKey{
{Field: store.PeerSortByLastSeen, Order: store.AscOrderBy},
{Field: store.PeerSortByName, Order: store.AscOrderBy},
{Field: store.PeerSortByVersion, Order: store.AscOrderBy},
},
})).Return(make([]*nbpeer.Peer, 0), 0, nil).AnyTimes()
},
},
{
name: "GetPeers sorting with invalid field",
requestPath: "/peers?sort_by=nosuchfield,asc",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers sorting with invalid field",
requestPath: "/peers?sort_by=nosuchfield,asc",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
{
name: "GetPeers sorting with invalid sorting order",
requestPath: "/peers?sort_by=name,nosuchorder&sort_by=version",
expectedStatus: http.StatusUnprocessableEntity,
setExpectations: func(p *Handler) {},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
p := initTestMetaData(t, peer)
recorder := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, tc.requestPath, nil)
req = nbcontext.SetUserAuthInRequest(req, auth.UserAuth{
UserId: "admin_user",
Domain: "hotmail.com",
AccountId: "test_id",
})
v1validator, err := apiv1alpha1.CreateV1ApiValidatingMiddleware()
assert.NoError(t, err)
router := mux.NewRouter()
router.Use(v1validator.Handler)
router = p.WithEndpointsForRouter(router)
tc.setExpectations(p)
router.ServeHTTP(recorder, req)
res := recorder.Result()
defer res.Body.Close()
assert.Equal(t, tc.expectedStatus, recorder.Code)
})
}
}