diff options
Diffstat (limited to 'internal/api')
-rw-r--r-- | internal/api/client/statuses/status_test.go | 114 | ||||
-rw-r--r-- | internal/api/client/statuses/statusboost_test.go | 751 | ||||
-rw-r--r-- | internal/api/client/statuses/statuscreate_test.go | 92 | ||||
-rw-r--r-- | internal/api/client/statuses/statusfave_test.go | 300 |
4 files changed, 916 insertions, 341 deletions
diff --git a/internal/api/client/statuses/status_test.go b/internal/api/client/statuses/status_test.go index a979f0c00..1a92276a1 100644 --- a/internal/api/client/statuses/status_test.go +++ b/internal/api/client/statuses/status_test.go @@ -18,6 +18,12 @@ package statuses_test import ( + "bytes" + "encoding/json" + "io" + "net/http/httptest" + "strings" + "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -25,6 +31,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/state" @@ -59,6 +66,113 @@ type StatusStandardTestSuite struct { statusModule *statuses.Module } +// Normalizes a status response to a determinate +// form, and pretty-prints it to JSON. +func (suite *StatusStandardTestSuite) parseStatusResponse( + recorder *httptest.ResponseRecorder, +) (string, *httptest.ResponseRecorder) { + + result := recorder.Result() + defer result.Body.Close() + + data, err := io.ReadAll(result.Body) + if err != nil { + suite.FailNow(err.Error()) + } + + rawMap := make(map[string]any) + if err := json.Unmarshal(data, &rawMap); err != nil { + suite.FailNow(err.Error()) + } + + // Make status fields determinate. + suite.determinateStatus(rawMap) + + // For readability, don't + // escape HTML, and indent json. + out := new(bytes.Buffer) + enc := json.NewEncoder(out) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + + if err := enc.Encode(&rawMap); err != nil { + suite.FailNow(err.Error()) + } + + return strings.TrimSpace(out.String()), recorder +} + +func (suite *StatusStandardTestSuite) determinateStatus(rawMap map[string]any) { + // Replace any fields from the raw map that + // aren't determinate (date, id, url, etc). + if _, ok := rawMap["id"]; ok { + rawMap["id"] = id.Highest + } + + if _, ok := rawMap["uri"]; ok { + rawMap["uri"] = "http://localhost:8080/some/determinate/url" + } + + if _, ok := rawMap["url"]; ok { + rawMap["url"] = "http://localhost:8080/some/determinate/url" + } + + if _, ok := rawMap["created_at"]; ok { + rawMap["created_at"] = "right the hell just now babyee" + } + + // Make ID of any mentions determinate. + if menchiesRaw, ok := rawMap["mentions"]; ok { + menchies, ok := menchiesRaw.([]any) + if !ok { + suite.FailNow("couldn't coerce menchies") + } + + for _, menchieRaw := range menchies { + menchie, ok := menchieRaw.(map[string]any) + if !ok { + suite.FailNow("couldn't coerce menchie") + } + + if _, ok := menchie["id"]; ok { + menchie["id"] = id.Highest + } + } + } + + // Make fields of any poll determinate. + if pollRaw, ok := rawMap["poll"]; ok && pollRaw != nil { + poll, ok := pollRaw.(map[string]any) + if !ok { + suite.FailNow("couldn't coerce poll") + } + + if _, ok := poll["id"]; ok { + poll["id"] = id.Highest + } + + if _, ok := poll["expires_at"]; ok { + poll["expires_at"] = "ah like you know whatever dude it's chill" + } + } + + // Replace account since that's not really + // what we care about for these tests. + if _, ok := rawMap["account"]; ok { + rawMap["account"] = "yeah this is my account, what about it punk" + } + + // If status contains an embedded + // reblog do the same thing for that. + if reblogRaw, ok := rawMap["reblog"]; ok && reblogRaw != nil { + reblog, ok := reblogRaw.(map[string]any) + if !ok { + suite.FailNow("couldn't coerce reblog") + } + suite.determinateStatus(reblog) + } +} + func (suite *StatusStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() suite.testClients = testrig.NewTestClients() diff --git a/internal/api/client/statuses/statusboost_test.go b/internal/api/client/statuses/statusboost_test.go index f6f589a5c..8642ba7aa 100644 --- a/internal/api/client/statuses/statusboost_test.go +++ b/internal/api/client/statuses/statusboost_test.go @@ -17,9 +17,6 @@ package statuses_test import ( "context" - "encoding/json" - "fmt" - "io/ioutil" "net/http" "net/http/httptest" "strings" @@ -28,7 +25,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" @@ -38,212 +35,596 @@ type StatusBoostTestSuite struct { StatusStandardTestSuite } -func (suite *StatusBoostTestSuite) TestPostBoost() { - t := suite.testTokens["local_account_1"] - oauthToken := oauth.DBTokenToToken(t) - - targetStatus := suite.testStatuses["admin_account_status_1"] - - // setup +func (suite *StatusBoostTestSuite) postStatusBoost( + targetStatusID string, + app *gtsmodel.Application, + token *gtsmodel.Token, + user *gtsmodel.User, + account *gtsmodel.Account, +) (string, *httptest.ResponseRecorder) { recorder := httptest.NewRecorder() ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) - ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Set(oauth.SessionAuthorizedApplication, app) + ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token)) + ctx.Set(oauth.SessionAuthorizedUser, user) + ctx.Set(oauth.SessionAuthorizedAccount, account) + + const pathBase = "http://localhost:8080/api" + statuses.ReblogPath + path := strings.ReplaceAll(pathBase, ":"+apiutil.IDKey, targetStatusID) + ctx.Request = httptest.NewRequest(http.MethodPost, path, nil) ctx.Request.Header.Set("accept", "application/json") - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. + // Populate target status ID. ctx.Params = gin.Params{ gin.Param{ - Key: statuses.IDKey, - Value: targetStatus.ID, + Key: apiutil.IDKey, + Value: targetStatusID, }, } + // Trigger handler. suite.statusModule.StatusBoostPOSTHandler(ctx) + return suite.parseStatusResponse(recorder) +} - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - - statusReply := &apimodel.Status{} - err = json.Unmarshal(b, statusReply) - suite.NoError(err) - - suite.False(statusReply.Sensitive) - suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility) - - suite.Empty(statusReply.SpoilerText) - suite.Empty(statusReply.Content) - suite.Equal("the_mighty_zork", statusReply.Account.Username) - suite.Len(statusReply.MediaAttachments, 0) - suite.Len(statusReply.Mentions, 0) - suite.Len(statusReply.Emojis, 0) - suite.Len(statusReply.Tags, 0) - - suite.NotNil(statusReply.Application) - suite.Equal("really cool gts application", statusReply.Application.Name) - - suite.NotNil(statusReply.Reblog) - suite.Equal(1, statusReply.Reblog.ReblogsCount) - suite.Equal(1, statusReply.Reblog.FavouritesCount) - suite.Equal(targetStatus.Content, statusReply.Reblog.Content) - suite.Equal(targetStatus.ContentWarning, statusReply.Reblog.SpoilerText) - suite.Equal(targetStatus.AccountID, statusReply.Reblog.Account.ID) - suite.Len(statusReply.Reblog.MediaAttachments, 1) - suite.Len(statusReply.Reblog.Tags, 1) - suite.Len(statusReply.Reblog.Emojis, 1) - suite.True(statusReply.Reblogged) - suite.True(statusReply.Reblog.Reblogged) - suite.Equal("superseriousbusiness", statusReply.Reblog.Application.Name) +func (suite *StatusBoostTestSuite) TestPostBoost() { + var ( + targetStatus = suite.testStatuses["admin_account_status_1"] + app = suite.testApplications["application_1"] + token = suite.testTokens["local_account_1"] + user = suite.testUsers["local_account_1"] + account = suite.testAccounts["local_account_1"] + ) + + out, recorder := suite.postStatusBoost( + targetStatus.ID, + app, + token, + user, + account, + ) + + // We should have OK from + // our call to the function. + suite.Equal(http.StatusOK, recorder.Code) + + // Target status should now + // be "reblogged" by us. + suite.Equal(`{ + "account": "yeah this is my account, what about it punk", + "application": { + "name": "really cool gts application", + "website": "https://reallycool.app" + }, + "bookmarked": true, + "card": null, + "content": "", + "created_at": "right the hell just now babyee", + "emojis": [], + "favourited": true, + "favourites_count": 0, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "interaction_policy": { + "can_favourite": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "public", + "me" + ], + "with_approval": [] + } + }, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "poll": null, + "reblog": { + "account": "yeah this is my account, what about it punk", + "application": { + "name": "superseriousbusiness", + "website": "https://superserious.business" + }, + "bookmarked": true, + "card": null, + "content": "hello world! #welcome ! first post on the instance :rainbow: !", + "created_at": "right the hell just now babyee", + "emojis": [ + { + "category": "reactions", + "shortcode": "rainbow", + "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", + "url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", + "visible_in_picker": true + } + ], + "favourited": true, + "favourites_count": 1, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "interaction_policy": { + "can_favourite": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "public", + "me" + ], + "with_approval": [] + } + }, + "language": "en", + "media_attachments": [ + { + "blurhash": "LIIE|gRj00WB-;j[t7j[4nWBj[Rj", + "description": "Black and white image of some 50's style text saying: Welcome On Board", + "id": "01F8MH6NEM8D7527KZAECTCR76", + "meta": { + "focus": { + "x": 0, + "y": 0 + }, + "original": { + "aspect": 1.9047619, + "height": 630, + "size": "1200x630", + "width": 1200 + }, + "small": { + "aspect": 1.9104477, + "height": 268, + "size": "512x268", + "width": 512 + } + }, + "preview_remote_url": null, + "preview_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.webp", + "remote_url": null, + "text_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", + "type": "image", + "url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg" + } + ], + "mentions": [], + "muted": false, + "pinned": false, + "poll": null, + "reblog": null, + "reblogged": true, + "reblogs_count": 1, + "replies_count": 1, + "sensitive": false, + "spoiler_text": "", + "tags": [ + { + "name": "welcome", + "url": "http://localhost:8080/tags/welcome" + } + ], + "text": "hello world! #welcome ! first post on the instance :rainbow: !", + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "public" + }, + "reblogged": true, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "public" +}`, out) } func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() { - t := suite.testTokens["local_account_1"] - oauthToken := oauth.DBTokenToToken(t) - - testStatus := suite.testStatuses["local_account_1_status_5"] - testAccount := suite.testAccounts["local_account_1"] - testUser := suite.testUsers["local_account_1"] - - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, testUser) - ctx.Set(oauth.SessionAuthorizedAccount, testAccount) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", testStatus.ID, 1)), nil) - ctx.Request.Header.Set("accept", "application/json") - - ctx.Params = gin.Params{ - gin.Param{ - Key: statuses.IDKey, - Value: testStatus.ID, - }, - } - - suite.statusModule.StatusBoostPOSTHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - - responseStatus := &apimodel.Status{} - err = json.Unmarshal(b, responseStatus) - suite.NoError(err) - - suite.False(responseStatus.Sensitive) - suite.Equal(suite.tc.VisToAPIVis(context.Background(), testStatus.Visibility), responseStatus.Visibility) - - suite.Empty(responseStatus.SpoilerText) - suite.Empty(responseStatus.Content) - suite.Equal("the_mighty_zork", responseStatus.Account.Username) - suite.Len(responseStatus.MediaAttachments, 0) - suite.Len(responseStatus.Mentions, 0) - suite.Len(responseStatus.Emojis, 0) - suite.Len(responseStatus.Tags, 0) - - suite.NotNil(responseStatus.Application) - suite.Equal("really cool gts application", responseStatus.Application.Name) - - suite.NotNil(responseStatus.Reblog) - suite.Equal(1, responseStatus.Reblog.ReblogsCount) - suite.Equal(0, responseStatus.Reblog.FavouritesCount) - suite.Equal(testStatus.Content, responseStatus.Reblog.Content) - suite.Equal(testStatus.ContentWarning, responseStatus.Reblog.SpoilerText) - suite.Equal(testStatus.AccountID, responseStatus.Reblog.Account.ID) - suite.Equal(suite.tc.VisToAPIVis(context.Background(), testStatus.Visibility), responseStatus.Reblog.Visibility) - suite.Empty(responseStatus.Reblog.MediaAttachments) - suite.Empty(responseStatus.Reblog.Tags) - suite.Empty(responseStatus.Reblog.Emojis) - suite.True(responseStatus.Reblogged) - suite.True(responseStatus.Reblog.Reblogged) - suite.Equal("really cool gts application", responseStatus.Reblog.Application.Name) + var ( + targetStatus = suite.testStatuses["local_account_1_status_5"] + app = suite.testApplications["application_1"] + token = suite.testTokens["local_account_1"] + user = suite.testUsers["local_account_1"] + account = suite.testAccounts["local_account_1"] + ) + + out, recorder := suite.postStatusBoost( + targetStatus.ID, + app, + token, + user, + account, + ) + + // We should have OK from + // our call to the function. + suite.Equal(http.StatusOK, recorder.Code) + + // Target status should now + // be "reblogged" by us. + suite.Equal(`{ + "account": "yeah this is my account, what about it punk", + "application": { + "name": "really cool gts application", + "website": "https://reallycool.app" + }, + "bookmarked": false, + "card": null, + "content": "", + "created_at": "right the hell just now babyee", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "interaction_policy": { + "can_favourite": { + "always": [ + "author", + "followers", + "mentioned", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "author", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "author", + "followers", + "mentioned", + "me" + ], + "with_approval": [] + } + }, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "poll": null, + "reblog": { + "account": "yeah this is my account, what about it punk", + "application": { + "name": "really cool gts application", + "website": "https://reallycool.app" + }, + "bookmarked": false, + "card": null, + "content": "hi!", + "created_at": "right the hell just now babyee", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "interaction_policy": { + "can_favourite": { + "always": [ + "author", + "followers", + "mentioned", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "author", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "author", + "followers", + "mentioned", + "me" + ], + "with_approval": [] + } + }, + "language": "en", + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "poll": null, + "reblog": null, + "reblogged": true, + "reblogs_count": 1, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": "hi!", + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "private" + }, + "reblogged": true, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "private" +}`, out) } -// try to boost a status that's not boostable / visible to us +// Try to boost a status that's +// not boostable / visible to us. func (suite *StatusBoostTestSuite) TestPostUnboostable() { - t := suite.testTokens["local_account_1"] - oauthToken := oauth.DBTokenToToken(t) - - targetStatus := suite.testStatuses["local_account_2_status_4"] - - // setup - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) - ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: statuses.IDKey, - Value: targetStatus.ID, - }, - } - - suite.statusModule.StatusBoostPOSTHandler(ctx) - - // check response + var ( + targetStatus = suite.testStatuses["local_account_2_status_4"] + app = suite.testApplications["application_1"] + token = suite.testTokens["local_account_1"] + user = suite.testUsers["local_account_1"] + account = suite.testAccounts["local_account_1"] + ) + + out, recorder := suite.postStatusBoost( + targetStatus.ID, + app, + token, + user, + account, + ) + + // We should have 403 from + // our call to the function. suite.Equal(http.StatusForbidden, recorder.Code) - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Equal(`{"error":"Forbidden: you do not have permission to boost this status"}`, string(b)) + // We should have a helpful message. + suite.Equal(`{ + "error": "Forbidden: you do not have permission to boost this status" +}`, out) } -// try to boost a status that's not visible to the user +// Try to boost a status that's not visible to the user. func (suite *StatusBoostTestSuite) TestPostNotVisible() { - // stop local_account_2 following zork - err := suite.db.DeleteByID(context.Background(), suite.testFollows["local_account_2_local_account_1"].ID, >smodel.Follow{}) - suite.NoError(err) - - t := suite.testTokens["local_account_2"] - oauthToken := oauth.DBTokenToToken(t) - - targetStatus := suite.testStatuses["local_account_1_status_3"] // this is a mutual only status and these accounts aren't mutuals - - // setup - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"]) - ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: statuses.IDKey, - Value: targetStatus.ID, - }, + // Stop local_account_2 following zork. + err := suite.db.DeleteFollowByID( + context.Background(), + suite.testFollows["local_account_2_local_account_1"].ID, + ) + if err != nil { + suite.FailNow(err.Error()) } - suite.statusModule.StatusBoostPOSTHandler(ctx) + var ( + // This is a mutual only status and + // these accounts aren't mutuals anymore. + targetStatus = suite.testStatuses["local_account_1_status_3"] + app = suite.testApplications["application_1"] + token = suite.testTokens["local_account_2"] + user = suite.testUsers["local_account_2"] + account = suite.testAccounts["local_account_2"] + ) + + out, recorder := suite.postStatusBoost( + targetStatus.ID, + app, + token, + user, + account, + ) + + // We should have 404 from + // our call to the function. + suite.Equal(http.StatusNotFound, recorder.Code) + + // We should have a helpful message. + suite.Equal(`{ + "error": "Not Found: target status not found" +}`, out) +} - // check response - suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible +// Boost a status that's pending approval by us. +func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() { + var ( + targetStatus = suite.testStatuses["admin_account_status_5"] + app = suite.testApplications["application_1"] + token = suite.testTokens["local_account_2"] + user = suite.testUsers["local_account_2"] + account = suite.testAccounts["local_account_2"] + ) + + out, recorder := suite.postStatusBoost( + targetStatus.ID, + app, + token, + user, + account, + ) + + // We should have OK from + // our call to the function. + suite.Equal(http.StatusOK, recorder.Code) + + // Target status should now + // be "reblogged" by us. + suite.Equal(`{ + "account": "yeah this is my account, what about it punk", + "application": { + "name": "really cool gts application", + "website": "https://reallycool.app" + }, + "bookmarked": false, + "card": null, + "content": "", + "created_at": "right the hell just now babyee", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "interaction_policy": { + "can_favourite": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "public", + "me" + ], + "with_approval": [] + } + }, + "language": null, + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "poll": null, + "reblog": { + "account": "yeah this is my account, what about it punk", + "application": { + "name": "superseriousbusiness", + "website": "https://superserious.business" + }, + "bookmarked": false, + "card": null, + "content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>", + "created_at": "right the hell just now babyee", + "emojis": [], + "favourited": false, + "favourites_count": 0, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF", + "in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5", + "interaction_policy": { + "can_favourite": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "public", + "me" + ], + "with_approval": [] + } + }, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "1happyturtle", + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "url": "http://localhost:8080/@1happyturtle", + "username": "1happyturtle" + } + ], + "muted": false, + "pinned": false, + "poll": null, + "reblog": null, + "reblogged": true, + "reblogs_count": 1, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": "Hi @1happyturtle, can I reply?", + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "unlisted" + }, + "reblogged": true, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "unlisted" +}`, out) + + // Target status should no + // longer be pending approval. + dbStatus, err := suite.state.DB.GetStatusByID( + context.Background(), + targetStatus.ID, + ) + if err != nil { + suite.FailNow(err.Error()) + } + suite.False(*dbStatus.PendingApproval) + + // There should be an Accept + // stored for the target status. + intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI( + context.Background(), targetStatus.URI, + ) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotZero(intReq.AcceptedAt) + suite.NotEmpty(intReq.URI) } func TestStatusBoostTestSuite(t *testing.T) { diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go index d32feb6c7..8598b5ef0 100644 --- a/internal/api/client/statuses/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -20,18 +20,14 @@ package statuses_test import ( "bytes" "context" - "encoding/json" "fmt" - "io" "net/http" "net/http/httptest" - "strings" "testing" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -81,91 +77,7 @@ func (suite *StatusCreateTestSuite) postStatus( // Trigger handler. suite.statusModule.StatusCreatePOSTHandler(ctx) - - result := recorder.Result() - defer result.Body.Close() - - data, err := io.ReadAll(result.Body) - if err != nil { - suite.FailNow(err.Error()) - } - - rawMap := make(map[string]any) - if err := json.Unmarshal(data, &rawMap); err != nil { - suite.FailNow(err.Error()) - } - - // Replace any fields from the raw map that - // aren't determinate (date, id, url, etc). - if _, ok := rawMap["id"]; ok { - rawMap["id"] = id.Highest - } - - if _, ok := rawMap["uri"]; ok { - rawMap["uri"] = "http://localhost:8080/some/determinate/url" - } - - if _, ok := rawMap["url"]; ok { - rawMap["url"] = "http://localhost:8080/some/determinate/url" - } - - if _, ok := rawMap["created_at"]; ok { - rawMap["created_at"] = "right the hell just now babyee" - } - - // Make ID of any mentions determinate. - if menchiesRaw, ok := rawMap["mentions"]; ok { - menchies, ok := menchiesRaw.([]any) - if !ok { - suite.FailNow("couldn't coerce menchies") - } - - for _, menchieRaw := range menchies { - menchie, ok := menchieRaw.(map[string]any) - if !ok { - suite.FailNow("couldn't coerce menchie") - } - - if _, ok := menchie["id"]; ok { - menchie["id"] = id.Highest - } - } - } - - // Make fields of any poll determinate. - if pollRaw, ok := rawMap["poll"]; ok && pollRaw != nil { - poll, ok := pollRaw.(map[string]any) - if !ok { - suite.FailNow("couldn't coerce poll") - } - - if _, ok := poll["id"]; ok { - poll["id"] = id.Highest - } - - if _, ok := poll["expires_at"]; ok { - poll["expires_at"] = "ah like you know whatever dude it's chill" - } - } - - // Replace account since that's not really - // what we care about for these tests. - if _, ok := rawMap["account"]; ok { - rawMap["account"] = "yeah this is my account, what about it punk" - } - - // For readability, don't - // escape HTML, and indent json. - out := new(bytes.Buffer) - enc := json.NewEncoder(out) - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - - if err := enc.Encode(&rawMap); err != nil { - suite.FailNow(err.Error()) - } - - return strings.TrimSpace(out.String()), recorder + return suite.parseStatusResponse(recorder) } // Post a new status with some custom visibility settings @@ -447,7 +359,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMessedUpIntPolicy() { suite.Equal(http.StatusBadRequest, recorder.Code) // We should have a helpful error - // message telling us how we screwed up. + // message telling us how we screwed up. suite.Equal(`{ "error": "Bad Request: error converting followers_only.can_reply.always: policyURI public is not feasible for visibility followers_only" }`, out) diff --git a/internal/api/client/statuses/statusfave_test.go b/internal/api/client/statuses/statusfave_test.go index d1042b10e..fdc8741c7 100644 --- a/internal/api/client/statuses/statusfave_test.go +++ b/internal/api/client/statuses/statusfave_test.go @@ -18,20 +18,18 @@ package statuses_test import ( - "encoding/json" - "fmt" - "io/ioutil" + "context" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/statuses" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -40,90 +38,260 @@ type StatusFaveTestSuite struct { StatusStandardTestSuite } -// fave a status -func (suite *StatusFaveTestSuite) TestPostFave() { - t := suite.testTokens["local_account_1"] - oauthToken := oauth.DBTokenToToken(t) - - targetStatus := suite.testStatuses["admin_account_status_2"] - - // setup +func (suite *StatusFaveTestSuite) postStatusFave( + targetStatusID string, + app *gtsmodel.Application, + token *gtsmodel.Token, + user *gtsmodel.User, + account *gtsmodel.Account, +) (string, *httptest.ResponseRecorder) { recorder := httptest.NewRecorder() ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) - ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Set(oauth.SessionAuthorizedApplication, app) + ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token)) + ctx.Set(oauth.SessionAuthorizedUser, user) + ctx.Set(oauth.SessionAuthorizedAccount, account) + + const pathBase = "http://localhost:8080/api" + statuses.FavouritePath + path := strings.ReplaceAll(pathBase, ":"+apiutil.IDKey, targetStatusID) + ctx.Request = httptest.NewRequest(http.MethodPost, path, nil) ctx.Request.Header.Set("accept", "application/json") - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. + // Populate target status ID. ctx.Params = gin.Params{ gin.Param{ - Key: statuses.IDKey, - Value: targetStatus.ID, + Key: apiutil.IDKey, + Value: targetStatusID, }, } + // Trigger handler. suite.statusModule.StatusFavePOSTHandler(ctx) + return suite.parseStatusResponse(recorder) +} - // check response - suite.EqualValues(http.StatusOK, recorder.Code) +// Fave a status we haven't faved yet. +func (suite *StatusFaveTestSuite) TestPostFave() { + var ( + targetStatus = suite.testStatuses["admin_account_status_2"] + app = suite.testApplications["application_1"] + token = suite.testTokens["local_account_1"] + user = suite.testUsers["local_account_1"] + account = suite.testAccounts["local_account_1"] + ) - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) + out, recorder := suite.postStatusFave( + targetStatus.ID, + app, + token, + user, + account, + ) - statusReply := &apimodel.Status{} - err = json.Unmarshal(b, statusReply) - assert.NoError(suite.T(), err) + // We should have OK from + // our call to the function. + suite.Equal(http.StatusOK, recorder.Code) - assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) - assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) - assert.True(suite.T(), statusReply.Sensitive) - assert.Equal(suite.T(), apimodel.VisibilityPublic, statusReply.Visibility) - assert.True(suite.T(), statusReply.Favourited) - assert.Equal(suite.T(), 1, statusReply.FavouritesCount) + // Target status should now + // be "favourited" by us. + suite.Equal(`{ + "account": "yeah this is my account, what about it punk", + "application": { + "name": "superseriousbusiness", + "website": "https://superserious.business" + }, + "bookmarked": false, + "card": null, + "content": "🐕🐕🐕🐕🐕", + "created_at": "right the hell just now babyee", + "emojis": [], + "favourited": true, + "favourites_count": 1, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": null, + "in_reply_to_id": null, + "interaction_policy": { + "can_favourite": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "public", + "me" + ], + "with_approval": [] + } + }, + "language": "en", + "media_attachments": [], + "mentions": [], + "muted": false, + "pinned": false, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": true, + "spoiler_text": "open to see some puppies", + "tags": [], + "text": "🐕🐕🐕🐕🐕", + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "public" +}`, out) } -// try to fave a status that's not faveable +// Try to fave a status +// that's not faveable by us. func (suite *StatusFaveTestSuite) TestPostUnfaveable() { - t := suite.testTokens["admin_account"] - oauthToken := oauth.DBTokenToToken(t) + var ( + targetStatus = suite.testStatuses["local_account_1_status_3"] + app = suite.testApplications["application_1"] + token = suite.testTokens["admin_account"] + user = suite.testUsers["admin_account"] + account = suite.testAccounts["admin_account"] + ) - targetStatus := suite.testStatuses["local_account_1_status_3"] // this one is unlikeable + out, recorder := suite.postStatusFave( + targetStatus.ID, + app, + token, + user, + account, + ) - // setup - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) - ctx.Set(oauth.SessionAuthorizedToken, oauthToken) - ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["admin_account"]) - ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["admin_account"]) - ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") + // We should have 403 from + // our call to the function. + suite.Equal(http.StatusForbidden, recorder.Code) - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: statuses.IDKey, - Value: targetStatus.ID, - }, - } + // We should get a helpful error. + suite.Equal(`{ + "error": "Forbidden: you do not have permission to fave this status" +}`, out) +} - suite.statusModule.StatusFavePOSTHandler(ctx) +// Fave a status that's pending approval by us. +func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() { + var ( + targetStatus = suite.testStatuses["admin_account_status_5"] + app = suite.testApplications["application_1"] + token = suite.testTokens["local_account_2"] + user = suite.testUsers["local_account_2"] + account = suite.testAccounts["local_account_2"] + ) - // check response - suite.EqualValues(http.StatusForbidden, recorder.Code) + out, recorder := suite.postStatusFave( + targetStatus.ID, + app, + token, + user, + account, + ) - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), `{"error":"Forbidden: you do not have permission to fave this status"}`, string(b)) + // We should have OK from + // our call to the function. + suite.Equal(http.StatusOK, recorder.Code) + + // Target status should now + // be "favourited" by us. + suite.Equal(`{ + "account": "yeah this is my account, what about it punk", + "application": { + "name": "superseriousbusiness", + "website": "https://superserious.business" + }, + "bookmarked": false, + "card": null, + "content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>", + "created_at": "right the hell just now babyee", + "emojis": [], + "favourited": true, + "favourites_count": 1, + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF", + "in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5", + "interaction_policy": { + "can_favourite": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reblog": { + "always": [ + "public", + "me" + ], + "with_approval": [] + }, + "can_reply": { + "always": [ + "public", + "me" + ], + "with_approval": [] + } + }, + "language": null, + "media_attachments": [], + "mentions": [ + { + "acct": "1happyturtle", + "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", + "url": "http://localhost:8080/@1happyturtle", + "username": "1happyturtle" + } + ], + "muted": false, + "pinned": false, + "poll": null, + "reblog": null, + "reblogged": false, + "reblogs_count": 0, + "replies_count": 0, + "sensitive": false, + "spoiler_text": "", + "tags": [], + "text": "Hi @1happyturtle, can I reply?", + "uri": "http://localhost:8080/some/determinate/url", + "url": "http://localhost:8080/some/determinate/url", + "visibility": "unlisted" +}`, out) + + // Target status should no + // longer be pending approval. + dbStatus, err := suite.state.DB.GetStatusByID( + context.Background(), + targetStatus.ID, + ) + if err != nil { + suite.FailNow(err.Error()) + } + suite.False(*dbStatus.PendingApproval) + + // There should be an Accept + // stored for the target status. + intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI( + context.Background(), targetStatus.URI, + ) + if err != nil { + suite.FailNow(err.Error()) + } + suite.NotZero(intReq.AcceptedAt) + suite.NotEmpty(intReq.URI) } func TestStatusFaveTestSuite(t *testing.T) { |