owpengram-server/internal/app/files/retention_purge_reap_test.go
2026-09-03 11:00:50 +03:00

150 lines
6.2 KiB
Go

package files
import (
"context"
"testing"
"time"
"go.uber.org/zap"
"telesrv/internal/domain"
"telesrv/internal/store"
)
// fakeReapStore is a minimal mediaRetentionStore fake exercising only
// notifyRetentionPurge's new reap step: ListMediaReferences reports one
// message_box reference, and orphanedDocument/orphanedPhoto control what
// OrphanDocumentIfUnreferenced/OrphanPhotoIfUnreferenced report -- simulating
// "the edit above dropped the last reference" (true) vs "something else
// still references it" (false, e.g. a live avatar or a failed edit). Every
// other mediaRetentionStore method is a stub never expected to be called by
// these tests -- but must still exist, or the type assertion
// s.media.(mediaRetentionStore) inside notifyRetentionPurge silently fails
// and the whole test becomes a no-op instead of exercising anything.
type fakeReapStore struct {
store.MediaStore
orphanedDocument bool
orphanedPhoto bool
deletedDocumentIDs []int64
deletedPhotoIDs []int64
}
func (f *fakeReapStore) ListMediaReferences(context.Context, domain.MediaKind, int64) ([]domain.MediaReference, error) {
return []domain.MediaReference{{RefKind: domain.MediaRefKindMessageBox, RefKey: "user:1:box:2"}}, nil
}
func (f *fakeReapStore) OrphanDocumentIfUnreferenced(context.Context, int64) (bool, error) {
return f.orphanedDocument, nil
}
func (f *fakeReapStore) OrphanPhotoIfUnreferenced(context.Context, int64) (bool, error) {
return f.orphanedPhoto, nil
}
func (f *fakeReapStore) DeleteDocumentAndBlobs(_ context.Context, id int64) ([]domain.FileBlob, error) {
f.deletedDocumentIDs = append(f.deletedDocumentIDs, id)
return nil, nil
}
func (f *fakeReapStore) DeletePhotoAndBlobs(_ context.Context, id int64) ([]domain.FileBlob, error) {
f.deletedPhotoIDs = append(f.deletedPhotoIDs, id)
return nil, nil
}
func (f *fakeReapStore) ListOrphanedDocumentIDsOlderThan(context.Context, domain.MediaCategory, time.Time, int) ([]int64, error) {
return nil, nil
}
func (f *fakeReapStore) ListOrphanedPhotoIDsOlderThan(context.Context, time.Time, int) ([]int64, error) {
return nil, nil
}
func (f *fakeReapStore) ListAvatarOrphanedPhotoIDsOlderThan(context.Context, time.Time, int) ([]int64, error) {
return nil, nil
}
func (f *fakeReapStore) CountFileBlobRefs(context.Context, string, string) (int, error) {
return 0, nil
}
func (f *fakeReapStore) ListDocumentIDsForHardRetentionOlderThan(context.Context, domain.MediaCategory, *time.Time, int) ([]int64, error) {
return nil, nil
}
func (f *fakeReapStore) ListPhotoIDsForHardRetentionOlderThan(context.Context, *time.Time, int) ([]int64, error) {
return nil, nil
}
func (f *fakeReapStore) ListAvatarPhotoIDsForHardRetentionOlderThan(context.Context, *time.Time, int) ([]int64, error) {
return nil, nil
}
func (f *fakeReapStore) CountDocumentsForHardRetention(context.Context, domain.MediaCategory, *time.Time) (int, error) {
return 0, nil
}
func (f *fakeReapStore) CountPhotosForHardRetention(context.Context, *time.Time) (int, error) {
return 0, nil
}
func (f *fakeReapStore) CountAvatarPhotosForHardRetention(context.Context, *time.Time) (int, error) {
return 0, nil
}
func (f *fakeReapStore) DeleteFileBlobsForDocument(context.Context, int64) ([]domain.FileBlob, error) {
return nil, nil
}
func (f *fakeReapStore) DeleteFileBlobsForPhoto(context.Context, int64) ([]domain.FileBlob, error) {
return nil, nil
}
func (f *fakeReapStore) SumFileBlobBytes(context.Context) (int64, error) { return 0, nil }
func (f *fakeReapStore) ListOldestMediaForEviction(context.Context, int) ([]domain.EvictionCandidate, error) {
return nil, nil
}
// fakeReapMessages is the minimal RetentionPurgeMessageEditor: GetMessages
// resolves a peer so notifyRetentionPurgeMessageBox can build the edit
// request, and EditMessage always succeeds.
type fakeReapMessages struct{}
func (fakeReapMessages) GetMessages(context.Context, int64, []int) (domain.MessageList, error) {
return domain.MessageList{Messages: []domain.Message{{ID: 2, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 3}}}}, nil
}
func (fakeReapMessages) EditMessage(context.Context, int64, domain.EditMessageRequest) (domain.EditMessageResult, error) {
return domain.EditMessageResult{}, nil
}
func newTestServiceForReap(media store.MediaStore) *Service {
s := &Service{media: media, log: zap.NewNop(), blobCache: newBlobMetaCache(8)}
s.SetRetentionPurgeNotifier(fakeReapMessages{}, nil)
return s
}
// TestNotifyRetentionPurgeReapsDocumentWhenFullyUnreferenced guards the fix
// for "documents/photos rows pile up forever even after the message showing
// them was already converted to the purge notice": once nothing references a
// purged document any more, notifyRetentionPurge must delete the row for
// real instead of always keeping it.
func TestNotifyRetentionPurgeReapsDocumentWhenFullyUnreferenced(t *testing.T) {
fake := &fakeReapStore{orphanedDocument: true}
s := newTestServiceForReap(fake)
s.notifyRetentionPurge(context.Background(), domain.MediaKindDocument, 42)
if len(fake.deletedDocumentIDs) != 1 || fake.deletedDocumentIDs[0] != 42 {
t.Fatalf("deleted document ids = %v, want [42]", fake.deletedDocumentIDs)
}
}
// TestNotifyRetentionPurgeKeepsDocumentWhenStillReferenced is the negative
// case: OrphanDocumentIfUnreferenced reporting false (something else -- a
// live avatar, sticker set, or a failed edit -- still references it) must
// leave the row alone, exactly like the safe orphan-mode deletion this reuses.
func TestNotifyRetentionPurgeKeepsDocumentWhenStillReferenced(t *testing.T) {
fake := &fakeReapStore{orphanedDocument: false}
s := newTestServiceForReap(fake)
s.notifyRetentionPurge(context.Background(), domain.MediaKindDocument, 42)
if len(fake.deletedDocumentIDs) != 0 {
t.Fatalf("deleted document ids = %v, want none", fake.deletedDocumentIDs)
}
}
// TestNotifyRetentionPurgeReapsPhotoWhenFullyUnreferenced is the photo
// counterpart of the document test above.
func TestNotifyRetentionPurgeReapsPhotoWhenFullyUnreferenced(t *testing.T) {
fake := &fakeReapStore{orphanedPhoto: true}
s := newTestServiceForReap(fake)
s.notifyRetentionPurge(context.Background(), domain.MediaKindPhoto, 99)
if len(fake.deletedPhotoIDs) != 1 || fake.deletedPhotoIDs[0] != 99 {
t.Fatalf("deleted photo ids = %v, want [99]", fake.deletedPhotoIDs)
}
}