Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ import (
type DsSearchRemoteScopes[C plugin.ToolLayerApiConnection, S plugin.ToolLayerScope] func(
apiClient plugin.ApiClient, params *models.DsRemoteApiScopeSearchParams) (children []models.DsRemoteApiScopeListEntry[S], err errors.Error)

// DsSearchRemoteScopesWithCount is like DsSearchRemoteScopes but additionally returns the total
// number of matching scopes reported by the remote server. A negative total means "unknown".
type DsSearchRemoteScopesWithCount[C plugin.ToolLayerApiConnection, S plugin.ToolLayerScope] func(
apiClient plugin.ApiClient, params *models.DsRemoteApiScopeSearchParams) (children []models.DsRemoteApiScopeListEntry[S], total int, err errors.Error)

type DsRemoteApiScopeSearchHelper[C plugin.ToolLayerApiConnection, S plugin.ToolLayerScope] struct {
*DsRemoteApiProxyHelper[C]
searchRemoteScopes DsSearchRemoteScopes[C, S]
searchRemoteScopes DsSearchRemoteScopesWithCount[C, S]
}

func NewDsRemoteApiScopeSearchHelper[
Expand All @@ -38,6 +43,25 @@ func NewDsRemoteApiScopeSearchHelper[
](
rap *DsRemoteApiProxyHelper[C],
searchRemoteScopes DsSearchRemoteScopes[C, S],
) *DsRemoteApiScopeSearchHelper[C, S] {
var withCount DsSearchRemoteScopesWithCount[C, S]
if searchRemoteScopes != nil {
withCount = func(apiClient plugin.ApiClient, params *models.DsRemoteApiScopeSearchParams) ([]models.DsRemoteApiScopeListEntry[S], int, errors.Error) {
children, err := searchRemoteScopes(apiClient, params)
return children, -1, err
}
}
return NewDsRemoteApiScopeSearchHelperWithCount[C, S](rap, withCount)
}

// NewDsRemoteApiScopeSearchHelperWithCount creates a search helper whose response additionally
// contains the total number of matching scopes ("count"), if the search function reports it.
func NewDsRemoteApiScopeSearchHelperWithCount[
C plugin.ToolLayerApiConnection,
S plugin.ToolLayerScope,
](
rap *DsRemoteApiProxyHelper[C],
searchRemoteScopes DsSearchRemoteScopesWithCount[C, S],
) *DsRemoteApiScopeSearchHelper[C, S] {
return &DsRemoteApiScopeSearchHelper[C, S]{
DsRemoteApiProxyHelper: rap,
Expand All @@ -62,22 +86,35 @@ func (rss *DsRemoteApiScopeSearchHelper[C, S]) Get(input *plugin.ApiResourceInpu
if e := vld.Struct(params); e != nil {
return nil, errors.BadInput.Wrap(e, "invalid params")
}
children, err := rss.searchRemoteScopes(apiClient, params)
children, total, err := rss.searchRemoteScopes(apiClient, params)
if err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{
Body: buildDsRemoteApiScopeSearchBody(children, total, params),
}, nil
}

func buildDsRemoteApiScopeSearchBody[S plugin.ToolLayerScope](
children []models.DsRemoteApiScopeListEntry[S],
total int,
params *models.DsRemoteApiScopeSearchParams,
) map[string]interface{} {
if children == nil {
children = []models.DsRemoteApiScopeListEntry[S]{}
}
// the config-ui is expecting the parent id to be null
for i := range children {
children[i].ParentId = nil
}
return &plugin.ApiResourceOutput{
Body: map[string]interface{}{
"children": children,
"page": params.Page,
"pageSize": params.PageSize,
},
}, nil
body := map[string]interface{}{
"children": children,
"page": params.Page,
"pageSize": params.PageSize,
}
// only expose the total if the plugin knows it, so the config-ui can fall back to its heuristic
if total >= 0 {
body["count"] = total
}
return body
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"testing"

"github.com/apache/devlake/core/errors"
"github.com/apache/devlake/core/plugin"
"github.com/apache/devlake/helpers/pluginhelper/api/models"
"github.com/stretchr/testify/assert"
)

type searchHelperTestScope = TestFakeGithubRepo

type searchHelperTestConnection struct{}

func (searchHelperTestConnection) TableName() string { return "_tool_test_connections" }
func (searchHelperTestConnection) ConnectionId() uint64 { return 1 }
func (searchHelperTestConnection) GetEndpoint() string { return "" }
func (searchHelperTestConnection) GetProxy() string { return "" }
func (searchHelperTestConnection) GetRateLimitPerHour() int { return 0 }

var _ plugin.ToolLayerApiConnection = searchHelperTestConnection{}
var _ plugin.ToolLayerScope = searchHelperTestScope{}

func TestBuildDsRemoteApiScopeSearchBody(t *testing.T) {
params := &models.DsRemoteApiScopeSearchParams{Search: "x", Page: 2, PageSize: 10}
parent := "parent"
children := []models.DsRemoteApiScopeListEntry[searchHelperTestScope]{{Id: "1", ParentId: &parent}}

body := buildDsRemoteApiScopeSearchBody(children, 42, params)
assert.Equal(t, 42, body["count"])
assert.Equal(t, 2, body["page"])
assert.Equal(t, 10, body["pageSize"])
assert.Nil(t, body["children"].([]models.DsRemoteApiScopeListEntry[searchHelperTestScope])[0].ParentId)

body = buildDsRemoteApiScopeSearchBody[searchHelperTestScope](nil, -1, params)
_, hasCount := body["count"]
assert.False(t, hasCount)
assert.NotNil(t, body["children"])
assert.Len(t, body["children"], 0)
}

func TestNewDsRemoteApiScopeSearchHelperWrapsLegacyFunction(t *testing.T) {
legacy := func(_ plugin.ApiClient, _ *models.DsRemoteApiScopeSearchParams) ([]models.DsRemoteApiScopeListEntry[searchHelperTestScope], errors.Error) {
return []models.DsRemoteApiScopeListEntry[searchHelperTestScope]{{Id: "1"}}, nil
}
helper := NewDsRemoteApiScopeSearchHelper[searchHelperTestConnection, searchHelperTestScope](nil, legacy)

children, total, err := helper.searchRemoteScopes(nil, &models.DsRemoteApiScopeSearchParams{})
assert.Nil(t, err)
assert.Len(t, children, 1)
assert.Equal(t, -1, total)

assert.Nil(t, NewDsRemoteApiScopeSearchHelper[searchHelperTestConnection, searchHelperTestScope](nil, nil).searchRemoteScopes)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ type DsRemoteApiScopeListEntry[S plugin.ToolLayerScope] struct {

type DsRemoteApiScopeList[S plugin.ToolLayerScope] struct {
Children []DsRemoteApiScopeListEntry[S] `json:"children"`
// Count is the total number of matching scopes (search only, omitted if the plugin doesn't know it)
Count *int `json:"count,omitempty"`
}
2 changes: 1 addition & 1 deletion backend/plugins/sonarqube/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ func Init(br context.BasicRes, p plugin.PluginMeta) {
models.SonarqubeProject,
SonarqubeRemotePagination,
](raProxy, listSonarqubeRemoteScopes)
raScopeSearch = api.NewDsRemoteApiScopeSearchHelper[models.SonarqubeConnection, models.SonarqubeProject](raProxy, searchSonarqubeRemoteProjects)
raScopeSearch = api.NewDsRemoteApiScopeSearchHelperWithCount[models.SonarqubeConnection, models.SonarqubeProject](raProxy, searchSonarqubeRemoteProjects)
}
8 changes: 6 additions & 2 deletions backend/plugins/sonarqube/api/remote_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func querySonarqubeProjects(
) (
children []dsmodels.DsRemoteApiScopeListEntry[models.SonarqubeProject],
nextPage *SonarqubeRemotePagination,
total int,
err errors.Error,
) {
if page.PageSize == 0 {
Expand Down Expand Up @@ -98,6 +99,7 @@ func querySonarqubeProjects(
})
}

total = resBody.Paging.Total
if resBody.Paging.Total > resBody.Paging.PageIndex*resBody.Paging.PageSize {
nextPage = &SonarqubeRemotePagination{
Page: resBody.Paging.PageIndex + 1,
Expand All @@ -118,7 +120,8 @@ func listSonarqubeRemoteScopes(
nextPage *SonarqubeRemotePagination,
err errors.Error,
) {
return querySonarqubeProjects(apiClient, "", page)
children, nextPage, _, err = querySonarqubeProjects(apiClient, "", page)
return
}

// RemoteScopes list all available scopes on the remote server
Expand All @@ -142,6 +145,7 @@ func searchSonarqubeRemoteProjects(
params *dsmodels.DsRemoteApiScopeSearchParams,
) (
children []dsmodels.DsRemoteApiScopeListEntry[models.SonarqubeProject],
total int,
err errors.Error,
) {
if params.Page == 0 {
Expand All @@ -151,7 +155,7 @@ func searchSonarqubeRemoteProjects(
Page: params.Page,
PageSize: params.PageSize,
}
children, _, err = querySonarqubeProjects(apiClient, params.Search, page)
children, _, total, err = querySonarqubeProjects(apiClient, params.Search, page)
return
}

Expand Down
41 changes: 40 additions & 1 deletion backend/plugins/sonarqube/api/remote_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,46 @@ limitations under the License.

package api

import "testing"
import (
"io"
"net/http"
"net/url"
"strings"
"testing"

"github.com/apache/devlake/core/errors"
dsmodels "github.com/apache/devlake/helpers/pluginhelper/api/models"
mockplugin "github.com/apache/devlake/mocks/core/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestSearchSonarqubeRemoteProjectsReturnsTotal(t *testing.T) {
apiClient := mockplugin.NewApiClient(t)
var gotQuery url.Values
apiClient.EXPECT().Get("components/search_projects", mock.Anything, mock.Anything).
RunAndReturn(func(_ string, query url.Values, _ http.Header) (*http.Response, errors.Error) {
gotQuery = query
body := `{"paging":{"pageIndex":2,"pageSize":100,"total":237},` +
`"components":[{"key":"k1","name":"proj-1"},{"key":"k2","name":"proj-2"}]}`
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body))}, nil
})

children, total, err := searchSonarqubeRemoteProjects(apiClient, &dsmodels.DsRemoteApiScopeSearchParams{
Search: "proj",
Page: 2,
PageSize: 100,
})

assert.Nil(t, err)
assert.Equal(t, 237, total)
assert.Len(t, children, 2)
assert.Equal(t, "k1", children[0].Id)
assert.Equal(t, "proj-1", children[0].Name)
assert.Equal(t, "2", gotQuery.Get("p"))
assert.Equal(t, "100", gotQuery.Get("ps"))
assert.Equal(t, `query = "proj"`, gotQuery.Get("filter"))
}

func TestSonarqubeSearchProjectsQueryFilter(t *testing.T) {
t.Parallel()
Expand Down
2 changes: 1 addition & 1 deletion config-ui/src/api/scope/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const searchRemote = (
plugin: string,
connectionId: ID,
data: SearchRemoteQuery,
): Promise<{ children: RemoteScope[]; count: number }> =>
): Promise<{ children: RemoteScope[]; count?: number }> =>
request(`/plugins/${plugin}/connections/${connectionId}/search-remote-scopes`, {
method: 'get',
data,
Expand Down
Loading