[bugfix] ensure timeline limit query is respected (#4141)

# Description

Fixes a bug in the new timeline code in which the limit query parameter wasn't always being fulfilled, in which case some clients like Tusky would then assume it didn't need to add a "load more" placeholder view even when there were more statuses to be loaded. This also fiddles around a bit in the logging middleware handler to add some more code comments, and add logging of full request URIs when it is safe to do so.

## Checklist

- [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md).
- [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat.
- [x] I/we have not leveraged AI to create the proposed changes.
- [x] I/we have performed a self-review of added code.
- [x] I/we have written code that is legible and maintainable by others.
- [x] I/we have commented the added code, particularly in hard-to-understand areas.
- [ ] I/we have made any necessary changes to documentation.
- [x] I/we have added tests that cover new code.
- [x] I/we have run tests and they pass locally with the changes.
- [x] I/we have run `go fmt ./...` and `golangci-lint run`.

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4141
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-05-06 13:30:23 +00:00 committed by tobi
commit 8264b63337
5 changed files with 172 additions and 65 deletions

View file

@ -18,11 +18,16 @@
package timeline
import (
"context"
"fmt"
"slices"
"testing"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/log"
"code.superseriousbusiness.org/gotosocial/internal/paging"
"codeberg.org/gruf/go-structr"
"github.com/stretchr/testify/assert"
)
@ -60,6 +65,46 @@ var testStatusMeta = []*StatusMeta{
},
}
func TestStatusTimelineLoadLimit(t *testing.T) {
var tt StatusTimeline
tt.Init(1000)
// Prepare new context for the duration of this test.
ctx, cncl := context.WithCancel(context.Background())
defer cncl()
// Clone the input test status data.
data := slices.Clone(testStatusMeta)
// Insert test data into timeline.
_ = tt.cache.Insert(data...)
// Manually mark timeline as 'preloaded'.
tt.preloader.CheckPreload(tt.preloader.Done)
// Craft a new page for selection,
// setting placeholder min / max values
// but in particular setting a limit
// HIGHER than currently cached values.
page := new(paging.Page)
page.Min = paging.MinID(id.Lowest)
page.Max = paging.MaxID(id.Highest)
page.Limit = len(data) + 10
// Load crafted page from the cache. This
// SHOULD load all cached entries, then
// generate an extra 10 statuses up to limit.
apiStatuses, _, _, err := tt.Load(ctx,
page,
loadGeneratedStatusPage,
loadStatusIDsFrom(data),
nil, // no filtering
func(status *gtsmodel.Status) (*apimodel.Status, error) { return new(apimodel.Status), nil },
)
assert.NoError(t, err)
assert.Len(t, apiStatuses, page.Limit)
}
func TestStatusTimelineUnprepare(t *testing.T) {
var tt StatusTimeline
tt.Init(1000)
@ -301,6 +346,44 @@ func TestStatusTimelineTrim(t *testing.T) {
assert.Equal(t, before, tt.cache.Len())
}
// loadStatusIDsFrom imitates loading of statuses of given IDs from the database, instead selecting
// statuses with appropriate IDs from the given slice of status meta, converting them to statuses.
func loadStatusIDsFrom(data []*StatusMeta) func(ids []string) ([]*gtsmodel.Status, error) {
return func(ids []string) ([]*gtsmodel.Status, error) {
var statuses []*gtsmodel.Status
for _, id := range ids {
i := slices.IndexFunc(data, func(s *StatusMeta) bool {
return s.ID == id
})
if i < 0 || i >= len(data) {
panic(fmt.Sprintf("could not find %s in %v", id, log.VarDump(data)))
}
statuses = append(statuses, &gtsmodel.Status{
ID: data[i].ID,
AccountID: data[i].AccountID,
BoostOfID: data[i].BoostOfID,
BoostOfAccountID: data[i].BoostOfAccountID,
})
}
return statuses, nil
}
}
// loadGeneratedStatusPage imitates loading of a given page of statuses,
// simply generating new statuses until the given page's limit is reached.
func loadGeneratedStatusPage(page *paging.Page) ([]*gtsmodel.Status, error) {
var statuses []*gtsmodel.Status
for range page.Limit {
statuses = append(statuses, &gtsmodel.Status{
ID: id.NewULID(),
AccountID: id.NewULID(),
BoostOfID: id.NewULID(),
BoostOfAccountID: id.NewULID(),
})
}
return statuses, nil
}
// containsStatusID returns whether timeline contains a status with ID.
func containsStatusID(t *StatusTimeline, id string) bool {
return getStatusByID(t, id) != nil