gotosocial/internal/cleaner/emoji_test.go

428 lines
11 KiB
Go

package cleaner_test
import (
"context"
"errors"
"time"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func copyMap(in map[string]*gtsmodel.Emoji) map[string]*gtsmodel.Emoji {
out := make(map[string]*gtsmodel.Emoji, len(in))
for k, v1 := range in {
v2 := new(gtsmodel.Emoji)
*v2 = *v1
out[k] = v2
}
return out
}
func (suite *CleanerTestSuite) TestEmojiUncacheRemote() {
suite.testEmojiUncacheRemote(
context.Background(),
mapvals(suite.emojis),
)
}
func (suite *CleanerTestSuite) TestEmojiUncacheRemoteDryRun() {
suite.testEmojiUncacheRemote(
gtscontext.SetDryRun(context.Background()),
mapvals(suite.emojis),
)
}
func (suite *CleanerTestSuite) TestEmojiFixBroken() {
suite.testEmojiFixBroken(
context.Background(),
mapvals(suite.emojis),
)
}
func (suite *CleanerTestSuite) TestEmojiFixBrokenDryRun() {
suite.testEmojiFixBroken(
gtscontext.SetDryRun(context.Background()),
mapvals(suite.emojis),
)
}
func (suite *CleanerTestSuite) TestEmojiPruneUnused() {
suite.testEmojiPruneUnused(
context.Background(),
mapvals(suite.emojis),
)
}
func (suite *CleanerTestSuite) TestEmojiPruneUnusedDryRun() {
suite.testEmojiPruneUnused(
gtscontext.SetDryRun(context.Background()),
mapvals(suite.emojis),
)
}
func (suite *CleanerTestSuite) TestEmojiFixCacheStates() {
// Copy testrig emojis + mark
// rainbow emoji as uncached
// so there's something to fix.
emojis := copyMap(suite.emojis)
emojis["rainbow"].Cached = util.Ptr(false)
suite.testEmojiFixCacheStates(
context.Background(),
mapvals(emojis),
)
}
func (suite *CleanerTestSuite) TestEmojiFixCacheStatesDryRun() {
// Copy testrig emojis + mark
// rainbow emoji as uncached
// so there's something to fix.
emojis := copyMap(suite.emojis)
emojis["rainbow"].Cached = util.Ptr(false)
suite.testEmojiFixCacheStates(
gtscontext.SetDryRun(context.Background()),
mapvals(emojis),
)
}
func (suite *CleanerTestSuite) testEmojiUncacheRemote(ctx context.Context, emojis []*gtsmodel.Emoji) {
var uncacheIDs []string
// Test state.
t := suite.T()
// Get max remote cache days to keep.
days := config.GetMediaRemoteCacheDays()
olderThan := time.Now().Add(-24 * time.Hour * time.Duration(days))
for _, emoji := range emojis {
// Check whether this emoji should be uncached.
ok, err := suite.shouldUncacheEmoji(ctx, emoji, olderThan)
if err != nil {
t.Fatalf("error checking whether emoji should be uncached: %v", err)
}
if ok {
// Mark this emoji ID as to be uncached.
uncacheIDs = append(uncacheIDs, emoji.ID)
}
}
// Attempt to uncache remote emojis.
found, err := suite.cleaner.Emoji().UncacheRemote(ctx, olderThan)
if err != nil {
t.Errorf("error uncaching remote emojis: %v", err)
return
}
// Check expected were uncached.
if found != len(uncacheIDs) {
t.Errorf("expected %d emojis to be uncached, %d were", len(uncacheIDs), found)
return
}
if gtscontext.DryRun(ctx) {
// nothing else to test.
return
}
for _, id := range uncacheIDs {
// Fetch the emoji by ID that should now be uncached.
emoji, err := suite.state.DB.GetEmojiByID(ctx, id)
if err != nil {
t.Fatalf("error fetching emoji from database: %v", err)
}
// Check cache state.
if *emoji.Cached {
t.Errorf("emoji %s@%s should have been uncached", emoji.Shortcode, emoji.Domain)
}
// Check that the emoji files in storage have been deleted.
if ok, err := suite.state.Storage.Has(ctx, emoji.ImagePath); err != nil {
t.Fatalf("error checking storage for emoji: %v", err)
} else if ok {
t.Errorf("emoji %s@%s image path should not exist", emoji.Shortcode, emoji.Domain)
} else if ok, err := suite.state.Storage.Has(ctx, emoji.ImageStaticPath); err != nil {
t.Fatalf("error checking storage for emoji: %v", err)
} else if ok {
t.Errorf("emoji %s@%s image static path should not exist", emoji.Shortcode, emoji.Domain)
}
}
}
func (suite *CleanerTestSuite) shouldUncacheEmoji(ctx context.Context, emoji *gtsmodel.Emoji, after time.Time) (bool, error) {
if emoji.ImageRemoteURL == "" {
// Local emojis are never uncached.
return false, nil
}
if emoji.Cached == nil || !*emoji.Cached {
// Emoji is already uncached.
return false, nil
}
// Get related accounts using this emoji (if any).
accounts, err := suite.state.DB.GetAccountsUsingEmoji(ctx, emoji.ID)
if err != nil {
return false, err
}
// Check if accounts are recently updated.
for _, account := range accounts {
if account.FetchedAt.After(after) {
return false, nil
}
}
// Get related statuses using this emoji (if any).
statuses, err := suite.state.DB.GetStatusesUsingEmoji(ctx, emoji.ID)
if err != nil {
return false, err
}
// Check if statuses are recently updated.
for _, status := range statuses {
if status.FetchedAt.After(after) {
return false, nil
}
}
return true, nil
}
func (suite *CleanerTestSuite) testEmojiFixBroken(ctx context.Context, emojis []*gtsmodel.Emoji) {
var fixIDs []string
// Test state.
t := suite.T()
for _, emoji := range emojis {
// Check whether this emoji should be fixed.
ok, err := suite.shouldFixBrokenEmoji(ctx, emoji)
if err != nil {
t.Fatalf("error checking whether emoji should be fixed: %v", err)
}
if ok {
// Mark this emoji ID as to be fixed.
fixIDs = append(fixIDs, emoji.ID)
}
}
// Attempt to fix broken emojis.
found, err := suite.cleaner.Emoji().FixBroken(ctx)
if err != nil {
t.Errorf("error fixing broken emojis: %v", err)
return
}
// Check expected were fixed.
if found != len(fixIDs) {
t.Errorf("expected %d emojis to be fixed, %d were", len(fixIDs), found)
return
}
if gtscontext.DryRun(ctx) {
// nothing else to test.
return
}
for _, id := range fixIDs {
// Fetch the emoji by ID that should now be fixed.
emoji, err := suite.state.DB.GetEmojiByID(ctx, id)
if err != nil {
t.Fatalf("error fetching emoji from database: %v", err)
}
// Ensure category was cleared.
if emoji.CategoryID != "" {
t.Errorf("emoji %s@%s should have empty category", emoji.Shortcode, emoji.Domain)
}
}
}
func (suite *CleanerTestSuite) shouldFixBrokenEmoji(ctx context.Context, emoji *gtsmodel.Emoji) (bool, error) {
if emoji.CategoryID == "" {
// no category issue.
return false, nil
}
// Get the related category for this emoji.
category, err := suite.state.DB.GetEmojiCategory(ctx, emoji.CategoryID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return false, nil
}
return (category == nil), nil
}
func (suite *CleanerTestSuite) testEmojiPruneUnused(ctx context.Context, emojis []*gtsmodel.Emoji) {
var pruneIDs []string
// Test state.
t := suite.T()
for _, emoji := range emojis {
// Check whether this emoji should be pruned.
ok, err := suite.shouldPruneEmoji(ctx, emoji)
if err != nil {
t.Fatalf("error checking whether emoji should be pruned: %v", err)
}
if ok {
// Mark this emoji ID as to be pruned.
pruneIDs = append(pruneIDs, emoji.ID)
}
}
// Attempt to prune emojis.
found, err := suite.cleaner.Emoji().PruneUnused(ctx)
if err != nil {
t.Errorf("error fixing broken emojis: %v", err)
return
}
// Check expected were pruned.
if found != len(pruneIDs) {
t.Errorf("expected %d emojis to be pruned, %d were", len(pruneIDs), found)
return
}
if gtscontext.DryRun(ctx) {
// nothing else to test.
return
}
for _, id := range pruneIDs {
// Fetch the emoji by ID that should now be pruned.
emoji, err := suite.state.DB.GetEmojiByID(ctx, id)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
t.Fatalf("error fetching emoji from database: %v", err)
}
// Ensure gone.
if emoji != nil {
t.Errorf("emoji %s@%s should have been pruned", emoji.Shortcode, emoji.Domain)
}
}
}
func (suite *CleanerTestSuite) shouldPruneEmoji(ctx context.Context, emoji *gtsmodel.Emoji) (bool, error) {
if emoji.ImageRemoteURL == "" {
// Local emojis are never pruned.
return false, nil
}
// Get related accounts using this emoji (if any).
accounts, err := suite.state.DB.GetAccountsUsingEmoji(ctx, emoji.ID)
if err != nil {
return false, err
} else if len(accounts) > 0 {
return false, nil
}
// Get related statuses using this emoji (if any).
statuses, err := suite.state.DB.GetStatusesUsingEmoji(ctx, emoji.ID)
if err != nil {
return false, err
} else if len(statuses) > 0 {
return false, nil
}
return true, nil
}
func (suite *CleanerTestSuite) testEmojiFixCacheStates(ctx context.Context, emojis []*gtsmodel.Emoji) {
var fixIDs []string
// Test state.
t := suite.T()
for _, emoji := range emojis {
// Check whether this emoji should be fixed.
ok, err := suite.shouldFixEmojiCacheState(ctx, emoji)
if err != nil {
t.Fatalf("error checking whether emoji should be fixed: %v", err)
}
if ok {
// Mark this emoji ID as to be fixed.
fixIDs = append(fixIDs, emoji.ID)
}
}
// Attempt to fix broken emoji cache states.
found, err := suite.cleaner.Emoji().FixCacheStates(ctx)
if err != nil {
t.Errorf("error fixing broken emojis: %v", err)
return
}
// Check expected were fixed.
if found != len(fixIDs) {
t.Errorf("expected %d emojis to be fixed, %d were", len(fixIDs), found)
return
}
if gtscontext.DryRun(ctx) {
// nothing else to test.
return
}
for _, id := range fixIDs {
// Fetch the emoji by ID that should now be fixed.
emoji, err := suite.state.DB.GetEmojiByID(ctx, id)
if err != nil {
t.Fatalf("error fetching emoji from database: %v", err)
}
// Ensure emoji cache state has been fixed.
ok, err := suite.shouldFixEmojiCacheState(ctx, emoji)
if err != nil {
t.Fatalf("error checking whether emoji should be fixed: %v", err)
} else if ok {
t.Errorf("emoji %s@%s cache state should have been fixed", emoji.Shortcode, emoji.Domain)
}
}
}
func (suite *CleanerTestSuite) shouldFixEmojiCacheState(ctx context.Context, emoji *gtsmodel.Emoji) (bool, error) {
// Check whether emoji image path exists.
haveImage, err := suite.state.Storage.Has(ctx, emoji.ImagePath)
if err != nil {
return false, err
}
// Check whether emoji static path exists.
haveStatic, err := suite.state.Storage.Has(ctx, emoji.ImageStaticPath)
if err != nil {
return false, err
}
switch exists := (haveImage && haveStatic); {
case emoji.Cached != nil &&
*emoji.Cached && !exists:
// (cached can be nil in tests)
// Cached but missing files.
return true, nil
case emoji.Cached != nil &&
!*emoji.Cached && exists:
// (cached can be nil in tests)
// Uncached but unexpected files.
return true, nil
default:
// No cache state issue.
return false, nil
}
}