summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/client/accounts/accountdelete_test.go6
-rw-r--r--internal/api/client/accounts/accounts.go86
-rw-r--r--internal/api/client/accounts/accountupdate_test.go2
-rw-r--r--internal/api/client/accounts/lookup.go93
-rw-r--r--internal/api/client/accounts/search.go166
-rw-r--r--internal/api/client/accounts/search_test.go430
-rw-r--r--internal/api/client/lists/listaccounts.go2
-rw-r--r--internal/api/client/search/search.go35
-rw-r--r--internal/api/client/search/searchget.go195
-rw-r--r--internal/api/client/search/searchget_test.go1070
-rw-r--r--internal/api/client/timelines/home.go2
-rw-r--r--internal/api/client/timelines/list.go2
-rw-r--r--internal/api/client/timelines/public.go2
-rw-r--r--internal/api/model/search.go78
-rw-r--r--internal/api/util/parsequery.go152
15 files changed, 1994 insertions, 327 deletions
diff --git a/internal/api/client/accounts/accountdelete_test.go b/internal/api/client/accounts/accountdelete_test.go
index fe328487b..d8889b680 100644
--- a/internal/api/client/accounts/accountdelete_test.go
+++ b/internal/api/client/accounts/accountdelete_test.go
@@ -44,7 +44,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandler() {
}
bodyBytes := requestBody.Bytes()
recorder := httptest.NewRecorder()
- ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeleteAccountPath, w.FormDataContentType())
+ ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeletePath, w.FormDataContentType())
// call the handler
suite.accountsModule.AccountDeletePOSTHandler(ctx)
@@ -66,7 +66,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerWrongPassword()
}
bodyBytes := requestBody.Bytes()
recorder := httptest.NewRecorder()
- ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeleteAccountPath, w.FormDataContentType())
+ ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeletePath, w.FormDataContentType())
// call the handler
suite.accountsModule.AccountDeletePOSTHandler(ctx)
@@ -86,7 +86,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerNoPassword() {
}
bodyBytes := requestBody.Bytes()
recorder := httptest.NewRecorder()
- ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeleteAccountPath, w.FormDataContentType())
+ ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, accounts.DeletePath, w.FormDataContentType())
// call the handler
suite.accountsModule.AccountDeletePOSTHandler(ctx)
diff --git a/internal/api/client/accounts/accounts.go b/internal/api/client/accounts/accounts.go
index 298104a8d..9bb13231d 100644
--- a/internal/api/client/accounts/accounts.go
+++ b/internal/api/client/accounts/accounts.go
@@ -25,53 +25,33 @@ import (
)
const (
- // LimitKey is for setting the return amount limit for eg., requesting an account's statuses
- LimitKey = "limit"
- // ExcludeRepliesKey is for specifying whether to exclude replies in a list of returned statuses by an account.
- ExcludeRepliesKey = "exclude_replies"
- // ExcludeReblogsKey is for specifying whether to exclude reblogs in a list of returned statuses by an account.
ExcludeReblogsKey = "exclude_reblogs"
- // PinnedKey is for specifying whether to include pinned statuses in a list of returned statuses by an account.
- PinnedKey = "pinned"
- // MaxIDKey is for specifying the maximum ID of the status to retrieve.
- MaxIDKey = "max_id"
- // MinIDKey is for specifying the minimum ID of the status to retrieve.
- MinIDKey = "min_id"
- // OnlyMediaKey is for specifying that only statuses with media should be returned in a list of returned statuses by an account.
- OnlyMediaKey = "only_media"
- // OnlyPublicKey is for specifying that only statuses with visibility public should be returned in a list of returned statuses by account.
- OnlyPublicKey = "only_public"
-
- // IDKey is the key to use for retrieving account ID in requests
- IDKey = "id"
- // BasePath is the base API path for this module, excluding the 'api' prefix
- BasePath = "/v1/accounts"
- // BasePathWithID is the base path for this module with the ID key
+ ExcludeRepliesKey = "exclude_replies"
+ LimitKey = "limit"
+ MaxIDKey = "max_id"
+ MinIDKey = "min_id"
+ OnlyMediaKey = "only_media"
+ OnlyPublicKey = "only_public"
+ PinnedKey = "pinned"
+
+ BasePath = "/v1/accounts"
+ IDKey = "id"
BasePathWithID = BasePath + "/:" + IDKey
- // VerifyPath is for verifying account credentials
- VerifyPath = BasePath + "/verify_credentials"
- // UpdateCredentialsPath is for updating account credentials
- UpdateCredentialsPath = BasePath + "/update_credentials"
- // GetStatusesPath is for showing an account's statuses
- GetStatusesPath = BasePathWithID + "/statuses"
- // GetFollowersPath is for showing an account's followers
- GetFollowersPath = BasePathWithID + "/followers"
- // GetFollowingPath is for showing account's that an account follows.
- GetFollowingPath = BasePathWithID + "/following"
- // GetRelationshipsPath is for showing an account's relationship with other accounts
- GetRelationshipsPath = BasePath + "/relationships"
- // FollowPath is for POSTing new follows to, and updating existing follows
- FollowPath = BasePathWithID + "/follow"
- // UnfollowPath is for POSTing an unfollow
- UnfollowPath = BasePathWithID + "/unfollow"
- // BlockPath is for creating a block of an account
- BlockPath = BasePathWithID + "/block"
- // UnblockPath is for removing a block of an account
- UnblockPath = BasePathWithID + "/unblock"
- // DeleteAccountPath is for deleting one's account via the API
- DeleteAccountPath = BasePath + "/delete"
- // ListsPath is for seeing which lists an account is.
- ListsPath = BasePathWithID + "/lists"
+
+ BlockPath = BasePathWithID + "/block"
+ DeletePath = BasePath + "/delete"
+ FollowersPath = BasePathWithID + "/followers"
+ FollowingPath = BasePathWithID + "/following"
+ FollowPath = BasePathWithID + "/follow"
+ ListsPath = BasePathWithID + "/lists"
+ LookupPath = BasePath + "/lookup"
+ RelationshipsPath = BasePath + "/relationships"
+ SearchPath = BasePath + "/search"
+ StatusesPath = BasePathWithID + "/statuses"
+ UnblockPath = BasePathWithID + "/unblock"
+ UnfollowPath = BasePathWithID + "/unfollow"
+ UpdatePath = BasePath + "/update_credentials"
+ VerifyPath = BasePath + "/verify_credentials"
)
type Module struct {
@@ -92,23 +72,23 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
attachHandler(http.MethodGet, BasePathWithID, m.AccountGETHandler)
// delete account
- attachHandler(http.MethodPost, DeleteAccountPath, m.AccountDeletePOSTHandler)
+ attachHandler(http.MethodPost, DeletePath, m.AccountDeletePOSTHandler)
// verify account
attachHandler(http.MethodGet, VerifyPath, m.AccountVerifyGETHandler)
// modify account
- attachHandler(http.MethodPatch, UpdateCredentialsPath, m.AccountUpdateCredentialsPATCHHandler)
+ attachHandler(http.MethodPatch, UpdatePath, m.AccountUpdateCredentialsPATCHHandler)
// get account's statuses
- attachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
+ attachHandler(http.MethodGet, StatusesPath, m.AccountStatusesGETHandler)
// get following or followers
- attachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
- attachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler)
+ attachHandler(http.MethodGet, FollowersPath, m.AccountFollowersGETHandler)
+ attachHandler(http.MethodGet, FollowingPath, m.AccountFollowingGETHandler)
// get relationship with account
- attachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
+ attachHandler(http.MethodGet, RelationshipsPath, m.AccountRelationshipsGETHandler)
// follow or unfollow account
attachHandler(http.MethodPost, FollowPath, m.AccountFollowPOSTHandler)
@@ -120,4 +100,8 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
// account lists
attachHandler(http.MethodGet, ListsPath, m.AccountListsGETHandler)
+
+ // search for accounts
+ attachHandler(http.MethodGet, SearchPath, m.AccountSearchGETHandler)
+ attachHandler(http.MethodGet, LookupPath, m.AccountLookupGETHandler)
}
diff --git a/internal/api/client/accounts/accountupdate_test.go b/internal/api/client/accounts/accountupdate_test.go
index f6bff4825..01d12ab27 100644
--- a/internal/api/client/accounts/accountupdate_test.go
+++ b/internal/api/client/accounts/accountupdate_test.go
@@ -76,7 +76,7 @@ func (suite *AccountUpdateTestSuite) updateAccount(
) (*apimodel.Account, error) {
// Initialize http test context.
recorder := httptest.NewRecorder()
- ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdateCredentialsPath, contentType)
+ ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdatePath, contentType)
// Trigger the handler.
suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx)
diff --git a/internal/api/client/accounts/lookup.go b/internal/api/client/accounts/lookup.go
new file mode 100644
index 000000000..4b31ea6cc
--- /dev/null
+++ b/internal/api/client/accounts/lookup.go
@@ -0,0 +1,93 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 accounts
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// AccountLookupGETHandler swagger:operation GET /api/v1/accounts/lookup accountLookupGet
+//
+// Quickly lookup a username to see if it is available, skipping WebFinger resolution.
+//
+// ---
+// tags:
+// - accounts
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: acct
+// type: string
+// description: The username or Webfinger address to lookup.
+// in: query
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - read:accounts
+//
+// responses:
+// '200':
+// name: lookup result
+// description: Result of the lookup.
+// schema:
+// "$ref": "#/definitions/account"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) AccountLookupGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ query, errWithCode := apiutil.ParseSearchLookup(c.Query(apiutil.SearchLookupKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ account, errWithCode := m.processor.Search().Lookup(c.Request.Context(), authed.Account, query)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, account)
+}
diff --git a/internal/api/client/accounts/search.go b/internal/api/client/accounts/search.go
new file mode 100644
index 000000000..c10fb2960
--- /dev/null
+++ b/internal/api/client/accounts/search.go
@@ -0,0 +1,166 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 accounts
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// AccountSearchGETHandler swagger:operation GET /api/v1/accounts/search accountSearchGet
+//
+// Search for accounts by username and/or display name.
+//
+// ---
+// tags:
+// - accounts
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: limit
+// type: integer
+// description: Number of results to try to return.
+// default: 40
+// maximum: 80
+// minimum: 1
+// in: query
+// -
+// name: offset
+// type: integer
+// description: >-
+// Page number of results to return (starts at 0).
+// This parameter is currently not used, offsets
+// over 0 will always return 0 results.
+// default: 0
+// maximum: 10
+// minimum: 0
+// in: query
+// -
+// name: q
+// type: string
+// description: |-
+// Query string to search for. This can be in the following forms:
+// - `@[username]` -- search for an account with the given username on any domain. Can return multiple results.
+// - `@[username]@[domain]` -- search for a remote account with exact username and domain. Will only ever return 1 result at most.
+// - any arbitrary string -- search for accounts containing the given string in their username or display name. Can return multiple results.
+// in: query
+// required: true
+// -
+// name: resolve
+// type: boolean
+// description: >-
+// If query is for `@[username]@[domain]`, or a URL, allow the GoToSocial instance to resolve
+// the search by making calls to remote instances (webfinger, ActivityPub, etc).
+// default: false
+// in: query
+// -
+// name: following
+// type: boolean
+// description: >-
+// Show only accounts that the requesting account follows. If this is set to `true`, then the GoToSocial instance
+// will enhance the search by also searching within account notes, not just in usernames and display names.
+// default: false
+// in: query
+//
+// security:
+// - OAuth2 Bearer:
+// - read:accounts
+//
+// responses:
+// '200':
+// name: search results
+// description: Results of the search.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/account"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) AccountSearchGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 40, 80, 1)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ offset, errWithCode := apiutil.ParseSearchOffset(c.Query(apiutil.SearchOffsetKey), 0, 10, 0)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ query, errWithCode := apiutil.ParseSearchQuery(c.Query(apiutil.SearchQueryKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ resolve, errWithCode := apiutil.ParseSearchResolve(c.Query(apiutil.SearchResolveKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ following, errWithCode := apiutil.ParseSearchFollowing(c.Query(apiutil.SearchFollowingKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ results, errWithCode := m.processor.Search().Accounts(
+ c.Request.Context(),
+ authed.Account,
+ query,
+ limit,
+ offset,
+ resolve,
+ following,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ c.JSON(http.StatusOK, results)
+}
diff --git a/internal/api/client/accounts/search_test.go b/internal/api/client/accounts/search_test.go
new file mode 100644
index 000000000..7d778f090
--- /dev/null
+++ b/internal/api/client/accounts/search_test.go
@@ -0,0 +1,430 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 accounts_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type AccountSearchTestSuite struct {
+ AccountStandardTestSuite
+}
+
+func (suite *AccountSearchTestSuite) getSearch(
+ requestingAccount *gtsmodel.Account,
+ token *gtsmodel.Token,
+ user *gtsmodel.User,
+ limit *int,
+ offset *int,
+ query string,
+ resolve *bool,
+ following *bool,
+ expectedHTTPStatus int,
+ expectedBody string,
+) ([]*apimodel.Account, error) {
+ var (
+ recorder = httptest.NewRecorder()
+ ctx, _ = testrig.CreateGinTestContext(recorder, nil)
+ requestURL = testrig.URLMustParse("/api" + accounts.BasePath + "/search")
+ queryParts []string
+ )
+
+ // Put the request together.
+ if limit != nil {
+ queryParts = append(queryParts, apiutil.LimitKey+"="+strconv.Itoa(*limit))
+ }
+
+ if offset != nil {
+ queryParts = append(queryParts, apiutil.SearchOffsetKey+"="+strconv.Itoa(*offset))
+ }
+
+ queryParts = append(queryParts, apiutil.SearchQueryKey+"="+url.QueryEscape(query))
+
+ if resolve != nil {
+ queryParts = append(queryParts, apiutil.SearchResolveKey+"="+strconv.FormatBool(*resolve))
+ }
+
+ if following != nil {
+ queryParts = append(queryParts, apiutil.SearchFollowingKey+"="+strconv.FormatBool(*following))
+ }
+
+ requestURL.RawQuery = strings.Join(queryParts, "&")
+ ctx.Request = httptest.NewRequest(http.MethodGet, requestURL.String(), nil)
+ ctx.Set(oauth.SessionAuthorizedAccount, requestingAccount)
+ ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
+ ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
+ ctx.Set(oauth.SessionAuthorizedUser, user)
+
+ // Trigger the function being tested.
+ suite.accountsModule.AccountSearchGETHandler(ctx)
+
+ // Read the result.
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ b, err := io.ReadAll(result.Body)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ errs := gtserror.MultiError{}
+
+ // Check expected code + body.
+ if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
+ errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
+ }
+
+ // If we got an expected body, return early.
+ if expectedBody != "" && string(b) != expectedBody {
+ errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
+ }
+
+ if err := errs.Combine(); err != nil {
+ suite.FailNow("", "%v (body %s)", err, string(b))
+ }
+
+ accounts := []*apimodel.Account{}
+ if err := json.Unmarshal(b, &accounts); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ return accounts, nil
+}
+
+func (suite *AccountSearchTestSuite) TestSearchZorkOK() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "zork"
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 1 {
+ suite.FailNow("", "expected length %d got %d", 1, l)
+ }
+}
+
+func (suite *AccountSearchTestSuite) TestSearchZorkExactOK() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@the_mighty_zork"
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 1 {
+ suite.FailNow("", "expected length %d got %d", 1, l)
+ }
+}
+
+func (suite *AccountSearchTestSuite) TestSearchZorkWithDomainOK() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@the_mighty_zork@localhost:8080"
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 1 {
+ suite.FailNow("", "expected length %d got %d", 1, l)
+ }
+}
+
+func (suite *AccountSearchTestSuite) TestSearchFossSatanNotFollowing() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "foss_satan"
+ following *bool = func() *bool { i := false; return &i }()
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 1 {
+ suite.FailNow("", "expected length %d got %d", 1, l)
+ }
+}
+
+func (suite *AccountSearchTestSuite) TestSearchFossSatanFollowing() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "foss_satan"
+ following *bool = func() *bool { i := true; return &i }()
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 0 {
+ suite.FailNow("", "expected length %d got %d", 0, l)
+ }
+}
+
+func (suite *AccountSearchTestSuite) TestSearchBonkersQuery() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "aaaaa@aaaaaaaaa@aaaaa **** this won't@ return anything!@!!"
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 0 {
+ suite.FailNow("", "expected length %d got %d", 0, l)
+ }
+}
+
+func (suite *AccountSearchTestSuite) TestSearchAFollowing() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "a"
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 5 {
+ suite.FailNow("", "expected length %d got %d", 5, l)
+ }
+
+ usernames := make([]string, 0, 5)
+ for _, account := range accounts {
+ usernames = append(usernames, account.Username)
+ }
+
+ suite.EqualValues([]string{"her_fuckin_maj", "foss_satan", "1happyturtle", "the_mighty_zork", "admin"}, usernames)
+}
+
+func (suite *AccountSearchTestSuite) TestSearchANotFollowing() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "a"
+ following *bool = func() *bool { i := true; return &i }()
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ accounts, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ limit,
+ offset,
+ query,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody,
+ )
+
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if l := len(accounts); l != 2 {
+ suite.FailNow("", "expected length %d got %d", 2, l)
+ }
+
+ usernames := make([]string, 0, 2)
+ for _, account := range accounts {
+ usernames = append(usernames, account.Username)
+ }
+
+ suite.EqualValues([]string{"1happyturtle", "admin"}, usernames)
+}
+
+func TestAccountSearchTestSuite(t *testing.T) {
+ suite.Run(t, new(AccountSearchTestSuite))
+}
diff --git a/internal/api/client/lists/listaccounts.go b/internal/api/client/lists/listaccounts.go
index 9e87c4130..da902384f 100644
--- a/internal/api/client/lists/listaccounts.go
+++ b/internal/api/client/lists/listaccounts.go
@@ -129,7 +129,7 @@ func (m *Module) ListAccountsGETHandler(c *gin.Context) {
return
}
- limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20, 40, 1)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/search/search.go b/internal/api/client/search/search.go
index eaa3102f9..219e30280 100644
--- a/internal/api/client/search/search.go
+++ b/internal/api/client/search/search.go
@@ -25,39 +25,8 @@ import (
)
const (
- // BasePathV1 is the base path for serving v1 of the search API, minus the 'api' prefix
- BasePathV1 = "/v1/search"
-
- // BasePathV2 is the base path for serving v2 of the search API, minus the 'api' prefix
- BasePathV2 = "/v2/search"
-
- // AccountIDKey -- If provided, statuses returned will be authored only by this account
- AccountIDKey = "account_id"
- // MaxIDKey -- Return results older than this id
- MaxIDKey = "max_id"
- // MinIDKey -- Return results immediately newer than this id
- MinIDKey = "min_id"
- // TypeKey -- Enum(accounts, hashtags, statuses)
- TypeKey = "type"
- // ExcludeUnreviewedKey -- Filter out unreviewed tags? Defaults to false. Use true when trying to find trending tags.
- ExcludeUnreviewedKey = "exclude_unreviewed"
- // QueryKey -- The search query
- QueryKey = "q"
- // ResolveKey -- Attempt WebFinger lookup. Defaults to false.
- ResolveKey = "resolve"
- // LimitKey -- Maximum number of results to load, per type. Defaults to 20. Max 40.
- LimitKey = "limit"
- // OffsetKey -- Offset in search results. Used for pagination. Defaults to 0.
- OffsetKey = "offset"
- // FollowingKey -- Only include accounts that the user is following. Defaults to false.
- FollowingKey = "following"
-
- // TypeAccounts --
- TypeAccounts = "accounts"
- // TypeHashtags --
- TypeHashtags = "hashtags"
- // TypeStatuses --
- TypeStatuses = "statuses"
+ BasePathV1 = "/v1/search" // Base path for serving v1 of the search API, minus the 'api' prefix.
+ BasePathV2 = "/v2/search" // Base path for serving v2 of the search API, minus the 'api' prefix.
)
type Module struct {
diff --git a/internal/api/client/search/searchget.go b/internal/api/client/search/searchget.go
index d129bf4d6..33a90e078 100644
--- a/internal/api/client/search/searchget.go
+++ b/internal/api/client/search/searchget.go
@@ -18,10 +18,7 @@
package search
import (
- "errors"
- "fmt"
"net/http"
- "strconv"
"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@@ -40,6 +37,98 @@ import (
// tags:
// - search
//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: max_id
+// type: string
+// description: >-
+// Return only items *OLDER* than the given max ID.
+// The item with the specified ID will not be included in the response.
+// Currently only used if 'type' is set to a specific type.
+// in: query
+// required: false
+// -
+// name: min_id
+// type: string
+// description: >-
+// Return only items *immediately newer* than the given min ID.
+// The item with the specified ID will not be included in the response.
+// Currently only used if 'type' is set to a specific type.
+// in: query
+// required: false
+// -
+// name: limit
+// type: integer
+// description: Number of each type of item to return.
+// default: 20
+// maximum: 40
+// minimum: 1
+// in: query
+// required: false
+// -
+// name: offset
+// type: integer
+// description: >-
+// Page number of results to return (starts at 0).
+// This parameter is currently not used, page by selecting
+// a specific query type and using maxID and minID instead.
+// default: 0
+// maximum: 10
+// minimum: 0
+// in: query
+// required: false
+// -
+// name: q
+// type: string
+// description: |-
+// Query string to search for. This can be in the following forms:
+// - `@[username]` -- search for an account with the given username on any domain. Can return multiple results.
+// - @[username]@[domain]` -- search for a remote account with exact username and domain. Will only ever return 1 result at most.
+// - `https://example.org/some/arbitrary/url` -- search for an account OR a status with the given URL. Will only ever return 1 result at most.
+// - any arbitrary string -- search for accounts or statuses containing the given string. Can return multiple results.
+// in: query
+// required: true
+// -
+// name: type
+// type: string
+// description: |-
+// Type of item to return. One of:
+// - `` -- empty string; return any/all results.
+// - `accounts` -- return account(s).
+// - `statuses` -- return status(es).
+// - `hashtags` -- return hashtag(s).
+// If `type` is specified, paging can be performed using max_id and min_id parameters.
+// If `type` is not specified, see the `offset` parameter for paging.
+// in: query
+// -
+// name: resolve
+// type: boolean
+// description: >-
+// If searching query is for `@[username]@[domain]`, or a URL, allow the GoToSocial
+// instance to resolve the search by making calls to remote instances (webfinger, ActivityPub, etc).
+// default: false
+// in: query
+// -
+// name: following
+// type: boolean
+// description: >-
+// If search type includes accounts, and search query is an arbitrary string, show only accounts
+// that the requesting account follows. If this is set to `true`, then the GoToSocial instance will
+// enhance the search by also searching within account notes, not just in usernames and display names.
+// default: false
+// in: query
+// -
+// name: exclude_unreviewed
+// type: boolean
+// description: >-
+// If searching for hashtags, exclude those not yet approved by instance admin.
+// Currently this parameter is unused.
+// default: false
+// in: query
+//
// security:
// - OAuth2 Bearer:
// - read:search
@@ -74,93 +163,55 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
return
}
- excludeUnreviewed := false
- excludeUnreviewedString := c.Query(ExcludeUnreviewedKey)
- if excludeUnreviewedString != "" {
- var err error
- excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20, 40, 1)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- query := c.Query(QueryKey)
- if query == "" {
- err := errors.New("query parameter q was empty")
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ offset, errWithCode := apiutil.ParseSearchOffset(c.Query(apiutil.SearchOffsetKey), 0, 10, 0)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
- resolve := false
- resolveString := c.Query(ResolveKey)
- if resolveString != "" {
- var err error
- resolve, err = strconv.ParseBool(resolveString)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", ResolveKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
+ query, errWithCode := apiutil.ParseSearchQuery(c.Query(apiutil.SearchQueryKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- limit := 2
- limitString := c.Query(LimitKey)
- if limitString != "" {
- i, err := strconv.ParseInt(limitString, 10, 32)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
- limit = int(i)
- }
- if limit > 40 {
- limit = 40
- }
- if limit < 1 {
- limit = 1
+ resolve, errWithCode := apiutil.ParseSearchResolve(c.Query(apiutil.SearchResolveKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- offset := 0
- offsetString := c.Query(OffsetKey)
- if offsetString != "" {
- i, err := strconv.ParseInt(offsetString, 10, 32)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", OffsetKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
- offset = int(i)
+ following, errWithCode := apiutil.ParseSearchFollowing(c.Query(apiutil.SearchFollowingKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- following := false
- followingString := c.Query(FollowingKey)
- if followingString != "" {
- var err error
- following, err = strconv.ParseBool(followingString)
- if err != nil {
- err := fmt.Errorf("error parsing %s: %s", FollowingKey, err)
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
- return
- }
+ excludeUnreviewed, errWithCode := apiutil.ParseSearchExcludeUnreviewed(c.Query(apiutil.SearchExcludeUnreviewedKey), false)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
}
- searchQuery := &apimodel.SearchQuery{
- AccountID: c.Query(AccountIDKey),
- MaxID: c.Query(MaxIDKey),
- MinID: c.Query(MinIDKey),
- Type: c.Query(TypeKey),
- ExcludeUnreviewed: excludeUnreviewed,
- Query: query,
- Resolve: resolve,
+ searchRequest := &apimodel.SearchRequest{
+ MaxID: c.Query(apiutil.MaxIDKey),
+ MinID: c.Query(apiutil.MinIDKey),
Limit: limit,
Offset: offset,
+ Query: query,
+ QueryType: c.Query(apiutil.SearchTypeKey),
+ Resolve: resolve,
Following: following,
+ ExcludeUnreviewed: excludeUnreviewed,
}
- results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery)
+ results, errWithCode := m.processor.Search().Get(c.Request.Context(), authed.Account, searchRequest)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/search/searchget_test.go b/internal/api/client/search/searchget_test.go
index fe817099f..9e0a8eb67 100644
--- a/internal/api/client/search/searchget_test.go
+++ b/internal/api/client/search/searchget_test.go
@@ -18,55 +18,174 @@
package search_test
import (
+ "context"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
+ "net/url"
+ "strconv"
+ "strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/search"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/testrig"
)
type SearchGetTestSuite struct {
SearchStandardTestSuite
}
-func (suite *SearchGetTestSuite) testSearch(query string, resolve bool, expectedHTTPStatus int) (*apimodel.SearchResult, error) {
- requestPath := fmt.Sprintf("%s?q=%s&resolve=%t", search.BasePathV1, query, resolve)
- recorder := httptest.NewRecorder()
+func (suite *SearchGetTestSuite) getSearch(
+ requestingAccount *gtsmodel.Account,
+ token *gtsmodel.Token,
+ user *gtsmodel.User,
+ maxID *string,
+ minID *string,
+ limit *int,
+ offset *int,
+ query string,
+ queryType *string,
+ resolve *bool,
+ following *bool,
+ expectedHTTPStatus int,
+ expectedBody string,
+) (*apimodel.SearchResult, error) {
+ var (
+ recorder = httptest.NewRecorder()
+ ctx, _ = testrig.CreateGinTestContext(recorder, nil)
+ requestURL = testrig.URLMustParse("/api" + search.BasePathV1)
+ queryParts []string
+ )
+
+ // Put the request together.
+ if maxID != nil {
+ queryParts = append(queryParts, apiutil.MaxIDKey+"="+url.QueryEscape(*maxID))
+ }
+
+ if minID != nil {
+ queryParts = append(queryParts, apiutil.MinIDKey+"="+url.QueryEscape(*minID))
+ }
+
+ if limit != nil {
+ queryParts = append(queryParts, apiutil.LimitKey+"="+strconv.Itoa(*limit))
+ }
+
+ if offset != nil {
+ queryParts = append(queryParts, apiutil.SearchOffsetKey+"="+strconv.Itoa(*offset))
+ }
+
+ queryParts = append(queryParts, apiutil.SearchQueryKey+"="+url.QueryEscape(query))
- ctx := suite.newContext(recorder, requestPath)
+ if queryType != nil {
+ queryParts = append(queryParts, apiutil.SearchTypeKey+"="+url.QueryEscape(*queryType))
+ }
+
+ if resolve != nil {
+ queryParts = append(queryParts, apiutil.SearchResolveKey+"="+strconv.FormatBool(*resolve))
+ }
+
+ if following != nil {
+ queryParts = append(queryParts, apiutil.SearchFollowingKey+"="+strconv.FormatBool(*following))
+ }
+
+ requestURL.RawQuery = strings.Join(queryParts, "&")
+ ctx.Request = httptest.NewRequest(http.MethodGet, requestURL.String(), nil)
+ ctx.Set(oauth.SessionAuthorizedAccount, requestingAccount)
+ ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
+ ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
+ ctx.Set(oauth.SessionAuthorizedUser, user)
+ // Trigger the function being tested.
suite.searchModule.SearchGETHandler(ctx)
+ // Read the result.
result := recorder.Result()
defer result.Body.Close()
+ b, err := io.ReadAll(result.Body)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ errs := gtserror.MultiError{}
+
+ // Check expected code + body.
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
- return nil, fmt.Errorf("expected %d got %d", expectedHTTPStatus, resultCode)
+ errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
}
- b, err := ioutil.ReadAll(result.Body)
- if err != nil {
- return nil, err
+ // If we got an expected body, return early.
+ if expectedBody != "" && string(b) != expectedBody {
+ errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
+ }
+
+ if err := errs.Combine(); err != nil {
+ suite.FailNow("", "%v (body %s)", err, string(b))
}
searchResult := &apimodel.SearchResult{}
if err := json.Unmarshal(b, searchResult); err != nil {
- return nil, err
+ suite.FailNow(err.Error())
}
return searchResult, nil
}
-func (suite *SearchGetTestSuite) TestSearchRemoteAccountByURI() {
- query := "https://unknown-instance.com/users/brand_new_person"
- resolve := true
+func (suite *SearchGetTestSuite) bodgeLocalInstance(domain string) {
+ // Set new host.
+ config.SetHost(domain)
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ // Copy instance account to not mess up other tests.
+ instanceAccount := &gtsmodel.Account{}
+ *instanceAccount = *suite.testAccounts["instance_account"]
+
+ // Set username of instance account to given domain.
+ instanceAccount.Username = domain
+ if err := suite.db.UpdateAccount(context.Background(), instanceAccount, "username"); err != nil {
+ suite.FailNow(err.Error())
+ }
+}
+
+func (suite *SearchGetTestSuite) TestSearchRemoteAccountByURI() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "https://unknown-instance.com/users/brand_new_person"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -80,10 +199,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByURI() {
}
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestring() {
- query := "@brand_new_person@unknown-instance.com"
- resolve := true
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "@brand_new_person@unknown-instance.com"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -97,10 +242,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestring() {
}
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringUppercase() {
- query := "@Some_User@example.org"
- resolve := true
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "@Some_User@example.org"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -114,10 +285,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringUppercase()
}
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringNoLeadingAt() {
- query := "brand_new_person@unknown-instance.com"
- resolve := true
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "brand_new_person@unknown-instance.com"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -131,10 +328,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringNoLeadingAt(
}
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringNoResolve() {
- query := "@brand_new_person@unknown-instance.com"
- resolve := false
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@brand_new_person@unknown-instance.com"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -143,10 +366,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringNoResolve()
}
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringSpecialChars() {
- query := "@üser@ëxample.org"
- resolve := false
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@üser@ëxample.org"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -158,10 +407,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringSpecialChars
}
func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringSpecialCharsPunycode() {
- query := "@üser@xn--xample-ova.org"
- resolve := false
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@üser@xn--xample-ova.org"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -173,10 +448,36 @@ func (suite *SearchGetTestSuite) TestSearchRemoteAccountByNamestringSpecialChars
}
func (suite *SearchGetTestSuite) TestSearchLocalAccountByNamestring() {
- query := "@the_mighty_zork"
- resolve := false
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@the_mighty_zork"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -190,10 +491,36 @@ func (suite *SearchGetTestSuite) TestSearchLocalAccountByNamestring() {
}
func (suite *SearchGetTestSuite) TestSearchLocalAccountByNamestringWithDomain() {
- query := "@the_mighty_zork@localhost:8080"
- resolve := false
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@the_mighty_zork@localhost:8080"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -207,10 +534,36 @@ func (suite *SearchGetTestSuite) TestSearchLocalAccountByNamestringWithDomain()
}
func (suite *SearchGetTestSuite) TestSearchNonexistingLocalAccountByNamestringResolveTrue() {
- query := "@somone_made_up@localhost:8080"
- resolve := true
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "@somone_made_up@localhost:8080"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -219,10 +572,36 @@ func (suite *SearchGetTestSuite) TestSearchNonexistingLocalAccountByNamestringRe
}
func (suite *SearchGetTestSuite) TestSearchLocalAccountByURI() {
- query := "http://localhost:8080/users/the_mighty_zork"
- resolve := false
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "http://localhost:8080/users/the_mighty_zork"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -235,11 +614,37 @@ func (suite *SearchGetTestSuite) TestSearchLocalAccountByURI() {
suite.NotNil(gotAccount)
}
-func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountByURI() {
- query := "http://localhost:8080/users/localhost:8080"
- resolve := false
-
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+func (suite *SearchGetTestSuite) TestSearchLocalAccountByURL() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "http://localhost:8080/@the_mighty_zork"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -252,57 +657,398 @@ func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountByURI() {
suite.NotNil(gotAccount)
}
-func (suite *SearchGetTestSuite) TestSearchLocalAccountByURL() {
- query := "http://localhost:8080/@the_mighty_zork"
- resolve := false
+func (suite *SearchGetTestSuite) TestSearchNonexistingLocalAccountByURL() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "http://localhost:8080/@the_shmighty_shmork"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ suite.Len(searchResult.Accounts, 0)
+}
+
+func (suite *SearchGetTestSuite) TestSearchStatusByURL() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042"
+ queryType *string = func() *string { i := "statuses"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
- if !suite.Len(searchResult.Accounts, 1) {
- suite.FailNow("expected 1 account in search results but got 0")
+ if !suite.Len(searchResult.Statuses, 1) {
+ suite.FailNow("expected 1 status in search results but got 0")
}
- gotAccount := searchResult.Accounts[0]
- suite.NotNil(gotAccount)
+ gotStatus := searchResult.Statuses[0]
+ suite.NotNil(gotStatus)
}
-func (suite *SearchGetTestSuite) TestSearchNonexistingLocalAccountByURL() {
- query := "http://localhost:8080/@the_shmighty_shmork"
- resolve := true
+func (suite *SearchGetTestSuite) TestSearchBlockedDomainURL() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "https://replyguys.com/@someone"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Len(searchResult.Accounts, 0)
+ suite.Len(searchResult.Statuses, 0)
+ suite.Len(searchResult.Hashtags, 0)
+}
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+func (suite *SearchGetTestSuite) TestSearchBlockedDomainNamestring() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "@someone@replyguys.com"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(searchResult.Accounts, 0)
+ suite.Len(searchResult.Statuses, 0)
+ suite.Len(searchResult.Hashtags, 0)
}
-func (suite *SearchGetTestSuite) TestSearchStatusByURL() {
- query := "https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042"
- resolve := true
+func (suite *SearchGetTestSuite) TestSearchAAny() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "a"
+ queryType *string = nil // Return anything.
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Len(searchResult.Accounts, 5)
+ suite.Len(searchResult.Statuses, 4)
+ suite.Len(searchResult.Hashtags, 0)
+}
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+func (suite *SearchGetTestSuite) TestSearchAAnyFollowingOnly() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "a"
+ queryType *string = nil // Return anything.
+ following *bool = func() *bool { i := true; return &i }()
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
- if !suite.Len(searchResult.Statuses, 1) {
- suite.FailNow("expected 1 status in search results but got 0")
+ suite.Len(searchResult.Accounts, 2)
+ suite.Len(searchResult.Statuses, 4)
+ suite.Len(searchResult.Hashtags, 0)
+}
+
+func (suite *SearchGetTestSuite) TestSearchAStatuses() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "a"
+ queryType *string = func() *string { i := "statuses"; return &i }() // Only statuses.
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
}
- gotStatus := searchResult.Statuses[0]
- suite.NotNil(gotStatus)
+ suite.Len(searchResult.Accounts, 0)
+ suite.Len(searchResult.Statuses, 4)
+ suite.Len(searchResult.Hashtags, 0)
}
-func (suite *SearchGetTestSuite) TestSearchBlockedDomainURL() {
- query := "https://replyguys.com/@someone"
- resolve := true
+func (suite *SearchGetTestSuite) TestSearchAAccounts() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "a"
+ queryType *string = func() *string { i := "accounts"; return &i }() // Only accounts.
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ suite.Len(searchResult.Accounts, 5)
+ suite.Len(searchResult.Statuses, 0)
+ suite.Len(searchResult.Hashtags, 0)
+}
+
+func (suite *SearchGetTestSuite) TestSearchAAccountsLimit1() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = func() *int { i := 1; return &i }()
+ offset *int = nil
+ resolve *bool = func() *bool { i := true; return &i }()
+ query = "a"
+ queryType *string = func() *string { i := "accounts"; return &i }() // Only accounts.
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Len(searchResult.Accounts, 1)
+ suite.Len(searchResult.Statuses, 0)
+ suite.Len(searchResult.Hashtags, 0)
+}
+
+func (suite *SearchGetTestSuite) TestSearchLocalInstanceAccountByURI() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "http://localhost:8080/users/localhost:8080"
+ queryType *string = func() *string { i := "accounts"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -312,11 +1058,89 @@ func (suite *SearchGetTestSuite) TestSearchBlockedDomainURL() {
suite.Len(searchResult.Hashtags, 0)
}
-func (suite *SearchGetTestSuite) TestSearchBlockedDomainNamestring() {
- query := "@someone@replyguys.com"
- resolve := true
+func (suite *SearchGetTestSuite) TestSearchInstanceAccountFull() {
+ // Namestring excludes ':' in usernames, so we
+ // need to fiddle with the instance account a
+ // bit to get it to look like a different domain.
+ newDomain := "example.org"
+ suite.bodgeLocalInstance(newDomain)
+
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@" + newDomain + "@" + newDomain
+ queryType *string = nil
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
- searchResult, err := suite.testSearch(query, resolve, http.StatusOK)
+ suite.Len(searchResult.Accounts, 0)
+ suite.Len(searchResult.Statuses, 0)
+ suite.Len(searchResult.Hashtags, 0)
+}
+
+func (suite *SearchGetTestSuite) TestSearchInstanceAccountPartial() {
+ // Namestring excludes ':' in usernames, so we
+ // need to fiddle with the instance account a
+ // bit to get it to look like a different domain.
+ newDomain := "example.org"
+ suite.bodgeLocalInstance(newDomain)
+
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "@" + newDomain
+ queryType *string = nil
+ following *bool = nil
+ expectedHTTPStatus = http.StatusOK
+ expectedBody = ""
+ )
+
+ searchResult, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
if err != nil {
suite.FailNow(err.Error())
}
@@ -326,6 +1150,78 @@ func (suite *SearchGetTestSuite) TestSearchBlockedDomainNamestring() {
suite.Len(searchResult.Hashtags, 0)
}
+func (suite *SearchGetTestSuite) TestSearchBadQueryType() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = "whatever"
+ queryType *string = func() *string { i := "aaaaaaaaaaa"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusBadRequest
+ expectedBody = `{"error":"Bad Request: search query type aaaaaaaaaaa was not recognized, valid options are ['', 'accounts', 'statuses', 'hashtags']"}`
+ )
+
+ _, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+}
+
+func (suite *SearchGetTestSuite) TestSearchEmptyQuery() {
+ var (
+ requestingAccount = suite.testAccounts["local_account_1"]
+ token = suite.testTokens["local_account_1"]
+ user = suite.testUsers["local_account_1"]
+ maxID *string = nil
+ minID *string = nil
+ limit *int = nil
+ offset *int = nil
+ resolve *bool = nil
+ query = ""
+ queryType *string = func() *string { i := "aaaaaaaaaaa"; return &i }()
+ following *bool = nil
+ expectedHTTPStatus = http.StatusBadRequest
+ expectedBody = `{"error":"Bad Request: required key q was not set or had empty value"}`
+ )
+
+ _, err := suite.getSearch(
+ requestingAccount,
+ token,
+ user,
+ maxID,
+ minID,
+ limit,
+ offset,
+ query,
+ queryType,
+ resolve,
+ following,
+ expectedHTTPStatus,
+ expectedBody)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+}
+
func TestSearchGetTestSuite(t *testing.T) {
suite.Run(t, &SearchGetTestSuite{})
}
diff --git a/internal/api/client/timelines/home.go b/internal/api/client/timelines/home.go
index f64d61287..c3f075d5e 100644
--- a/internal/api/client/timelines/home.go
+++ b/internal/api/client/timelines/home.go
@@ -118,7 +118,7 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
return
}
- limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20, 40, 1)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/timelines/list.go b/internal/api/client/timelines/list.go
index 4f5232d8b..8b4f7fad9 100644
--- a/internal/api/client/timelines/list.go
+++ b/internal/api/client/timelines/list.go
@@ -125,7 +125,7 @@ func (m *Module) ListTimelineGETHandler(c *gin.Context) {
return
}
- limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20, 40, 1)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/timelines/public.go b/internal/api/client/timelines/public.go
index 5be9fcaa8..96958e6a4 100644
--- a/internal/api/client/timelines/public.go
+++ b/internal/api/client/timelines/public.go
@@ -129,7 +129,7 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
return
}
- limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20)
+ limit, errWithCode := apiutil.ParseLimit(c.Query(apiutil.LimitKey), 20, 40, 1)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/model/search.go b/internal/api/model/search.go
index f7b86f884..664bf7b26 100644
--- a/internal/api/model/search.go
+++ b/internal/api/model/search.go
@@ -17,74 +17,24 @@
package model
-// SearchQuery models a search request.
-//
-// swagger:parameters searchGet
-type SearchQuery struct {
- // If type is `statuses`, then statuses returned will be authored only by this account.
- //
- // in: query
- AccountID string `json:"account_id"`
- // Return results *older* than this id.
- //
- // The entry with this ID will not be included in the search results.
- // in: query
- MaxID string `json:"max_id"`
- // Return results *newer* than this id.
- //
- // The entry with this ID will not be included in the search results.
- // in: query
- MinID string `json:"min_id"`
- // Type of the search query to perform.
- //
- // Must be one of: `accounts`, `hashtags`, `statuses`.
- //
- // enum:
- // - accounts
- // - hashtags
- // - statuses
- // required: true
- // in: query
- Type string `json:"type"`
- // Filter out tags that haven't been reviewed and approved by an instance admin.
- //
- // default: false
- // in: query
- ExcludeUnreviewed bool `json:"exclude_unreviewed"`
- // String to use as a search query.
- //
- // For accounts, this should be in the format `@someaccount@some.instance.com`, or the format `https://some.instance.com/@someaccount`
- //
- // For a status, this can be in the format: `https://some.instance.com/@someaccount/SOME_ID_OF_A_STATUS`
- //
- // required: true
- // in: query
- Query string `json:"q"`
- // Attempt to resolve the query by performing a remote webfinger lookup, if the query includes a remote host.
- // default: false
- Resolve bool `json:"resolve"`
- // Maximum number of results to load, per type.
- // default: 20
- // minimum: 1
- // maximum: 40
- // in: query
- Limit int `json:"limit"`
- // Offset for paginating search results.
- //
- // default: 0
- // in: query
- Offset int `json:"offset"`
- // Only include accounts that the searching account is following.
- // default: false
- // in: query
- Following bool `json:"following"`
+// SearchRequest models a search request.
+type SearchRequest struct {
+ MaxID string
+ MinID string
+ Limit int
+ Offset int
+ Query string
+ QueryType string
+ Resolve bool
+ Following bool
+ ExcludeUnreviewed bool
}
// SearchResult models a search result.
//
// swagger:model searchResult
type SearchResult struct {
- Accounts []Account `json:"accounts"`
- Statuses []Status `json:"statuses"`
- Hashtags []Tag `json:"hashtags"`
+ Accounts []*Account `json:"accounts"`
+ Statuses []*Status `json:"statuses"`
+ Hashtags []*Tag `json:"hashtags"`
}
diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go
index 92578a739..460ca3e05 100644
--- a/internal/api/util/parsequery.go
+++ b/internal/api/util/parsequery.go
@@ -25,34 +25,162 @@ import (
)
const (
+ /* Common keys */
+
LimitKey = "limit"
LocalKey = "local"
+ MaxIDKey = "max_id"
+ MinIDKey = "min_id"
+
+ /* Search keys */
+
+ SearchExcludeUnreviewedKey = "exclude_unreviewed"
+ SearchFollowingKey = "following"
+ SearchLookupKey = "acct"
+ SearchOffsetKey = "offset"
+ SearchQueryKey = "q"
+ SearchResolveKey = "resolve"
+ SearchTypeKey = "type"
)
-func ParseLimit(limit string, defaultLimit int) (int, gtserror.WithCode) {
- if limit == "" {
- return defaultLimit, nil
+// parseError returns gtserror.WithCode set to 400 Bad Request, to indicate
+// to the caller that a key was set to a value that could not be parsed.
+func parseError(key string, value, defaultValue any, err error) gtserror.WithCode {
+ err = fmt.Errorf("error parsing key %s with value %s as %T: %w", key, value, defaultValue, err)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+}
+
+func requiredError(key string) gtserror.WithCode {
+ err := fmt.Errorf("required key %s was not set or had empty value", key)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+}
+
+/*
+ Parse functions for *OPTIONAL* parameters with default values.
+*/
+
+func ParseLimit(value string, defaultValue int, max, min int) (int, gtserror.WithCode) {
+ key := LimitKey
+
+ if value == "" {
+ return defaultValue, nil
}
- i, err := strconv.Atoi(limit)
+ i, err := strconv.Atoi(value)
if err != nil {
- err := fmt.Errorf("error parsing %s: %w", LimitKey, err)
- return 0, gtserror.NewErrorBadRequest(err, err.Error())
+ return defaultValue, parseError(key, value, defaultValue, err)
+ }
+
+ if i > max {
+ i = max
+ } else if i < min {
+ i = min
}
return i, nil
}
-func ParseLocal(local string, defaultLocal bool) (bool, gtserror.WithCode) {
- if local == "" {
- return defaultLocal, nil
+func ParseLocal(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ key := LimitKey
+
+ if value == "" {
+ return defaultValue, nil
}
- i, err := strconv.ParseBool(local)
+ i, err := strconv.ParseBool(value)
if err != nil {
- err := fmt.Errorf("error parsing %s: %w", LocalKey, err)
- return false, gtserror.NewErrorBadRequest(err, err.Error())
+ return defaultValue, parseError(key, value, defaultValue, err)
}
return i, nil
}
+
+func ParseSearchExcludeUnreviewed(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ key := SearchExcludeUnreviewedKey
+
+ if value == "" {
+ return defaultValue, nil
+ }
+
+ i, err := strconv.ParseBool(value)
+ if err != nil {
+ return defaultValue, parseError(key, value, defaultValue, err)
+ }
+
+ return i, nil
+}
+
+func ParseSearchFollowing(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ key := SearchFollowingKey
+
+ if value == "" {
+ return defaultValue, nil
+ }
+
+ i, err := strconv.ParseBool(value)
+ if err != nil {
+ return defaultValue, parseError(key, value, defaultValue, err)
+ }
+
+ return i, nil
+}
+
+func ParseSearchOffset(value string, defaultValue int, max, min int) (int, gtserror.WithCode) {
+ key := SearchOffsetKey
+
+ if value == "" {
+ return defaultValue, nil
+ }
+
+ i, err := strconv.Atoi(value)
+ if err != nil {
+ return defaultValue, parseError(key, value, defaultValue, err)
+ }
+
+ if i > max {
+ i = max
+ } else if i < min {
+ i = min
+ }
+
+ return i, nil
+}
+
+func ParseSearchResolve(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ key := SearchResolveKey
+
+ if value == "" {
+ return defaultValue, nil
+ }
+
+ i, err := strconv.ParseBool(value)
+ if err != nil {
+ return defaultValue, parseError(key, value, defaultValue, err)
+ }
+
+ return i, nil
+}
+
+/*
+ Parse functions for *REQUIRED* parameters.
+*/
+
+func ParseSearchLookup(value string) (string, gtserror.WithCode) {
+ key := SearchLookupKey
+
+ if value == "" {
+ return "", requiredError(key)
+ }
+
+ return value, nil
+}
+
+func ParseSearchQuery(value string) (string, gtserror.WithCode) {
+ key := SearchQueryKey
+
+ if value == "" {
+ return "", requiredError(key)
+ }
+
+ return value, nil
+}