diff options
Diffstat (limited to 'internal/api/client/instance')
-rw-r--r-- | internal/api/client/instance/instance.go | 23 | ||||
-rw-r--r-- | internal/api/client/instance/instance_test.go | 3 | ||||
-rw-r--r-- | internal/api/client/instance/instanceget.go | 18 | ||||
-rw-r--r-- | internal/api/client/instance/instancepatch.go | 18 | ||||
-rw-r--r-- | internal/api/client/instance/instancepatch_test.go | 6 | ||||
-rw-r--r-- | internal/api/client/instance/instancepeersget.go | 136 | ||||
-rw-r--r-- | internal/api/client/instance/instancepeersget_test.go | 247 |
7 files changed, 446 insertions, 5 deletions
diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go index 758cce376..16ff7c9f9 100644 --- a/internal/api/client/instance/instance.go +++ b/internal/api/client/instance/instance.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + package instance import ( @@ -11,6 +29,10 @@ import ( const ( // InstanceInformationPath is for serving instance info requests InstanceInformationPath = "api/v1/instance" + // InstancePeersPath is for serving instance peers requests. + InstancePeersPath = InstanceInformationPath + "/peers" + // PeersFilterKey is used to provide filters to /api/v1/instance/peers + PeersFilterKey = "filter" ) // Module implements the ClientModule interface @@ -29,5 +51,6 @@ func New(processor processing.Processor) api.ClientModule { func (m *Module) Route(s router.Router) error { s.AttachHandler(http.MethodGet, InstanceInformationPath, m.InstanceInformationGETHandler) s.AttachHandler(http.MethodPatch, InstanceInformationPath, m.InstanceUpdatePATCHHandler) + s.AttachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler) return nil } diff --git a/internal/api/client/instance/instance_test.go b/internal/api/client/instance/instance_test.go index 248b87761..1e5c59c97 100644 --- a/internal/api/client/instance/instance_test.go +++ b/internal/api/client/instance/instance_test.go @@ -21,7 +21,6 @@ package instance_test import ( "bytes" "fmt" - "net/http" "net/http/httptest" "codeberg.org/gruf/go-store/kv" @@ -113,7 +112,7 @@ func (suite *InstanceStandardTestSuite) newContext(recorder *httptest.ResponseRe baseURI := fmt.Sprintf("%s://%s", protocol, host) requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath) - ctx.Request = httptest.NewRequest(http.MethodPatch, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting + ctx.Request = httptest.NewRequest(requestMethod, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting if bodyContentType != "" { ctx.Request.Header.Set("Content-Type", bodyContentType) diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go index 35e842102..5250b3b46 100644 --- a/internal/api/client/instance/instanceget.go +++ b/internal/api/client/instance/instanceget.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + package instance import ( diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go index 4e3f1e454..6f1b3586b 100644 --- a/internal/api/client/instance/instancepatch.go +++ b/internal/api/client/instance/instancepatch.go @@ -1,3 +1,21 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + package instance import ( diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 5ca4f2b7a..683fec5a6 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -63,7 +63,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b)) + suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch2() { @@ -93,7 +93,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"uri":"http://localhost:8080","title":"Geoff's Instance","description":"","short_description":"","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) + suite.Equal(`{"uri":"http://localhost:8080","title":"Geoff's Instance","description":"","short_description":"","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch3() { @@ -123,7 +123,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) + suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch4() { diff --git a/internal/api/client/instance/instancepeersget.go b/internal/api/client/instance/instancepeersget.go new file mode 100644 index 000000000..d4d33d5bf --- /dev/null +++ b/internal/api/client/instance/instancepeersget.go @@ -0,0 +1,136 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package instance + +import ( + "fmt" + "net/http" + "strings" + + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + + "github.com/gin-gonic/gin" +) + +// InstancePeersGETHandler swagger:operation GET /api/v1/instance/peers instancePeersGet +// +// --- +// tags: +// - instance +// +// produces: +// - application/json +// +// parameters: +// - name: filter +// type: string +// description: |- +// Comma-separated list of filters to apply to results. Recognized values are: +// 'open' -- include peers that are not suspended or silenced +// 'suspended' -- include peers that have been suspended. +// If filter is 'open', only instances that haven't been suspended or silenced will be returned. +// If filter is 'suspended', only suspended instances will be shown. +// If filter is 'open,suspended', then all known instances will be returned. +// If filter is an empty string or not set, then 'open' will be assumed as the default. +// in: query +// required: false +// +// responses: +// '200': +// description: |- +// If no filter parameter is provided, or filter is empty, then a legacy, +// Mastodon-API compatible response will be returned. This will consist of +// just a 'flat' array of strings like `["example.com", "example.org"]`. +// +// If a filter parameter is provided, then an array of objects with at least +// a `domain` key set on each object will be returned. +// +// Domains that are silenced or suspended will also have a key +// 'suspended_at' or 'silenced_at' that contains an iso8601 date string. +// If one of these keys is not present on the domain object, it is open. +// Suspended instances may in some cases be obfuscated, which means they +// will have some letters replaced by '*' to make it more difficult for +// bad actors to target instances with harassment. +// +// Whether a flat response or a more detailed response is returned, domains +// will be sorted alphabetically by hostname. +// schema: +// type: array +// items: +// "$ref": "#/definitions/domain" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) InstancePeersGETHandler(c *gin.Context) { + authed, err := oauth.Authed(c, false, false, false, false) + if err != nil { + api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet) + return + } + + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) + return + } + + var includeSuspended bool + var includeOpen bool + var flat bool + if filterParam := c.Query(PeersFilterKey); filterParam != "" { + filters := strings.Split(filterParam, ",") + for _, f := range filters { + trimmed := strings.TrimSpace(f) + switch { + case strings.EqualFold(trimmed, "suspended"): + includeSuspended = true + case strings.EqualFold(trimmed, "open"): + includeOpen = true + default: + err := fmt.Errorf("filter %s not recognized; accepted values are 'open', 'suspended'", trimmed) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + return + } + } + } else { + // default is to only include open domains, and present + // them in a 'flat' manner (just an array of strings), + // to maintain compatibility with mastodon API + includeOpen = true + flat = true + } + + data, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), authed, includeSuspended, includeOpen, flat) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + return + } + + c.JSON(http.StatusOK, data) +} diff --git a/internal/api/client/instance/instancepeersget_test.go b/internal/api/client/instance/instancepeersget_test.go new file mode 100644 index 000000000..cb35a9e50 --- /dev/null +++ b/internal/api/client/instance/instancepeersget_test.go @@ -0,0 +1,247 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package instance_test + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/api/client/instance" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type InstancePeersGetTestSuite struct { + InstanceStandardTestSuite +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetNoParams() { + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s", baseURI, instance.InstancePeersPath) + ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`["example.org","fossbros-anonymous.io"]`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetNoParamsUnauthorized() { + config.SetInstanceExposePeers(false) + + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s", baseURI, instance.InstancePeersPath) + ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusUnauthorized, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"Unauthorized: peers open query requires an authenticated account/user"}`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetNoParamsAuthorized() { + config.SetInstanceExposePeers(false) + + recorder := httptest.NewRecorder() + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s", baseURI, instance.InstancePeersPath) + ctx := suite.newContext(recorder, http.MethodGet, []byte{}, requestURI, "") + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`["example.org","fossbros-anonymous.io"]`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspended() { + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=suspended", baseURI, instance.InstancePeersPath) + ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`[{"domain":"replyguys.com","suspended_at":"2020-05-13T13:29:12.000Z","public_comment":"reply-guying to tech posts"}]`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedUnauthorized() { + config.SetInstanceExposeSuspended(false) + + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=suspended", baseURI, instance.InstancePeersPath) + ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusUnauthorized, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"Unauthorized: peers suspended query requires an authenticated account/user"}`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedAuthorized() { + config.SetInstanceExposeSuspended(false) + + recorder := httptest.NewRecorder() + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=suspended", baseURI, instance.InstancePeersPath) + ctx := suite.newContext(recorder, http.MethodGet, []byte{}, requestURI, "") + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`[{"domain":"replyguys.com","suspended_at":"2020-05-13T13:29:12.000Z","public_comment":"reply-guying to tech posts"}]`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAll() { + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=suspended,open", baseURI, instance.InstancePeersPath) + ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`[{"domain":"example.org"},{"domain":"fossbros-anonymous.io"},{"domain":"replyguys.com","suspended_at":"2020-05-13T13:29:12.000Z","public_comment":"reply-guying to tech posts"}]`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated() { + err := suite.db.Put(context.Background(), >smodel.DomainBlock{ + ID: "01G633XTNK51GBADQZFZQDP6WR", + CreatedAt: testrig.TimeMustParse("2021-06-09T12:34:55+02:00"), + UpdatedAt: testrig.TimeMustParse("2021-06-09T12:34:55+02:00"), + Domain: "omg.just.the.worst.org.ever", + CreatedByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", + PublicComment: "just absolutely the worst, wowza", + Obfuscate: true, + }) + suite.NoError(err) + + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=suspended,open", baseURI, instance.InstancePeersPath) + ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`[{"domain":"example.org"},{"domain":"fossbros-anonymous.io"},{"domain":"o*g.*u**.t**.*or*t.*r**ev**","suspended_at":"2021-06-09T10:34:55.000Z","public_comment":"just absolutely the worst, wowza"},{"domain":"replyguys.com","suspended_at":"2020-05-13T13:29:12.000Z","public_comment":"reply-guying to tech posts"}]`, string(b)) +} + +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetFunkyParams() { + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=aaaaaaaaaaaaaaaaa,open", baseURI, instance.InstancePeersPath) + ctx.Request = httptest.NewRequest(http.MethodGet, requestURI, nil) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusBadRequest, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"Bad Request: filter aaaaaaaaaaaaaaaaa not recognized; accepted values are 'open', 'suspended'"}`, string(b)) +} + +func TestInstancePeersGetTestSuite(t *testing.T) { + suite.Run(t, &InstancePeersGetTestSuite{}) +} |