owpengram-server/internal/app/files/seed_test.go
2026-09-09 02:49:30 +03:00

1280 lines
44 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package files
import (
"bytes"
"context"
"fmt"
"image/png"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"testing"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
)
// fakeMediaStore 是 store.MediaStore 的内存替身,用于在无 PG 时验证 seed 导入器。
type fakeMediaStore struct {
mu sync.Mutex
blobs map[string]domain.FileBlob
docs map[int64]domain.Document
photos map[int64]domain.Photo
sets map[int64]domain.StickerSet
reactions []domain.AvailableReaction
parts map[string][]domain.UploadPart
webPages map[int64]domain.MessageWebPage
seedState map[string]string
receipts map[string]domain.UploadedMediaReceipt
// profilePhotos[ownerID|kind] 保存某 owner 当前 profile/fallback 照片引用。
profilePhotos map[string]domain.ProfilePhotoRef
}
func newFakeMediaStore() *fakeMediaStore {
return &fakeMediaStore{
blobs: map[string]domain.FileBlob{},
docs: map[int64]domain.Document{},
photos: map[int64]domain.Photo{},
sets: map[int64]domain.StickerSet{},
parts: map[string][]domain.UploadPart{},
seedState: map[string]string{},
receipts: map[string]domain.UploadedMediaReceipt{},
profilePhotos: map[string]domain.ProfilePhotoRef{},
}
}
func fakeProfilePhotoKey(ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) string {
return fmt.Sprintf("%s:%d:%s", ownerType, ownerID, kind)
}
func fakeUploadReceiptKey(ownerUserID, fileID int64) string {
return fmt.Sprintf("%d/%d", ownerUserID, fileID)
}
func (f *fakeMediaStore) GetUploadedMediaReceipt(_ context.Context, ownerUserID, fileID int64) (domain.UploadedMediaReceipt, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
receipt, ok := f.receipts[fakeUploadReceiptKey(ownerUserID, fileID)]
receipt.IntentHash = append([]byte(nil), receipt.IntentHash...)
return receipt, ok, nil
}
func (f *fakeMediaStore) PutUploadedMediaReceipt(_ context.Context, receipt domain.UploadedMediaReceipt) (domain.UploadedMediaReceipt, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
key := fakeUploadReceiptKey(receipt.OwnerUserID, receipt.FileID)
if stored, ok := f.receipts[key]; ok {
stored.IntentHash = append([]byte(nil), stored.IntentHash...)
return stored, false, nil
}
receipt.IntentHash = append([]byte(nil), receipt.IntentHash...)
f.receipts[key] = receipt
return receipt, true, nil
}
func (f *fakeMediaStore) SaveFilePart(_ context.Context, part domain.UploadPart) error {
f.mu.Lock()
defer f.mu.Unlock()
key := fakeUploadPartKey(part.OwnerUserID, part.FileID)
part.SHA256 = append([]byte(nil), part.SHA256...)
parts := f.parts[key]
for i := range parts {
if parts[i].Part == part.Part {
parts[i] = part
f.parts[key] = parts
return nil
}
}
f.parts[key] = append(parts, part)
return nil
}
func (f *fakeMediaStore) UploadPartUsage(_ context.Context, ownerUserID int64) (domain.UploadPartUsage, error) {
f.mu.Lock()
defer f.mu.Unlock()
var usage domain.UploadPartUsage
files := map[int64]struct{}{}
for _, parts := range f.parts {
for _, p := range parts {
if p.OwnerUserID != ownerUserID {
continue
}
usage.Bytes += p.Size
usage.Parts++
files[p.FileID] = struct{}{}
}
}
usage.Files = len(files)
return usage, nil
}
func (f *fakeMediaStore) UploadPartSlot(_ context.Context, ownerUserID, fileID int64, part int) (domain.UploadPartSlot, error) {
f.mu.Lock()
defer f.mu.Unlock()
parts := f.parts[fakeUploadPartKey(ownerUserID, fileID)]
slot := domain.UploadPartSlot{FileParts: len(parts)}
for _, p := range parts {
if p.Part == part {
slot.ExistingBytes = p.Size
slot.ObjectKey = p.ObjectKey
slot.Found = true
break
}
}
return slot, nil
}
func (f *fakeMediaStore) LoadFileParts(_ context.Context, ownerUserID, fileID int64) ([]domain.UploadPart, error) {
f.mu.Lock()
defer f.mu.Unlock()
parts := append([]domain.UploadPart(nil), f.parts[fakeUploadPartKey(ownerUserID, fileID)]...)
sort.Slice(parts, func(i, j int) bool { return parts[i].Part < parts[j].Part })
return parts, nil
}
func (f *fakeMediaStore) DeleteFileParts(_ context.Context, ownerUserID, fileID int64) ([]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
key := fakeUploadPartKey(ownerUserID, fileID)
parts := f.parts[key]
keys := make([]string, 0, len(parts))
for _, p := range parts {
keys = append(keys, p.ObjectKey)
}
delete(f.parts, key)
return keys, nil
}
func (f *fakeMediaStore) DeleteExpiredUploadParts(_ context.Context, _ time.Time, _ int) ([]string, error) {
return nil, nil
}
func fakeUploadPartKey(ownerUserID, fileID int64) string {
return fmt.Sprintf("%d:%d", ownerUserID, fileID)
}
func (f *fakeMediaStore) PutFileBlob(_ context.Context, blob domain.FileBlob) error {
f.mu.Lock()
defer f.mu.Unlock()
f.blobs[blob.LocationKey] = blob
return nil
}
func (f *fakeMediaStore) GetFileBlob(_ context.Context, key string) (domain.FileBlob, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
b, ok := f.blobs[key]
return b, ok, nil
}
func (f *fakeMediaStore) DeleteFileBlobRow(_ context.Context, key string) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.blobs, key)
return nil
}
func (f *fakeMediaStore) GetFileBlobs(_ context.Context, keys []string) (map[string]domain.FileBlob, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := make(map[string]domain.FileBlob, len(keys))
for _, key := range keys {
if b, ok := f.blobs[key]; ok {
out[key] = b
}
}
return out, nil
}
func (f *fakeMediaStore) SumFileBlobBytes(_ context.Context) (int64, error) {
f.mu.Lock()
defer f.mu.Unlock()
var total int64
for _, b := range f.blobs {
total += b.Size
}
return total, nil
}
func (f *fakeMediaStore) GetSeedState(_ context.Context, key string) (string, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
hash, ok := f.seedState[key]
return hash, ok, nil
}
func (f *fakeMediaStore) PutSeedState(_ context.Context, key, hash string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.seedState[key] = hash
return nil
}
func (f *fakeMediaStore) PutDocument(_ context.Context, doc domain.Document) error {
f.mu.Lock()
defer f.mu.Unlock()
f.docs[doc.ID] = doc
return nil
}
func (f *fakeMediaStore) GetDocument(_ context.Context, id int64) (domain.Document, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
d, ok := f.docs[id]
return d, ok, nil
}
func (f *fakeMediaStore) GetDocuments(_ context.Context, ids []int64) ([]domain.Document, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := make([]domain.Document, 0, len(ids))
for _, id := range ids {
if d, ok := f.docs[id]; ok {
out = append(out, d)
}
}
return out, nil
}
func (f *fakeMediaStore) PutPhoto(_ context.Context, p domain.Photo) error {
f.mu.Lock()
defer f.mu.Unlock()
f.photos[p.ID] = p
return nil
}
func (f *fakeMediaStore) GetPhoto(_ context.Context, id int64) (domain.Photo, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
p, ok := f.photos[id]
return p, ok, nil
}
func (f *fakeMediaStore) PutWebPage(_ context.Context, urlHash int64, page domain.MessageWebPage, _ int) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.webPages == nil {
f.webPages = map[int64]domain.MessageWebPage{}
}
f.webPages[urlHash] = page
return nil
}
func (f *fakeMediaStore) GetWebPageByURLHash(_ context.Context, urlHash int64) (domain.MessageWebPage, int, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
p, ok := f.webPages[urlHash]
return p, 0, ok, nil
}
func (f *fakeMediaStore) PutStickerSet(_ context.Context, set domain.StickerSet) error {
f.mu.Lock()
defer f.mu.Unlock()
f.sets[set.ID] = set
return nil
}
func (f *fakeMediaStore) CreateStickerSet(_ context.Context, set domain.StickerSet, docs []domain.Document) error {
f.mu.Lock()
defer f.mu.Unlock()
for _, existing := range f.sets {
if existing.ShortName != "" && strings.EqualFold(existing.ShortName, set.ShortName) {
return domain.ErrStickerSetShortNameOccupied
}
}
f.sets[set.ID] = set
if f.docs == nil {
f.docs = map[int64]domain.Document{}
}
for _, doc := range docs {
f.docs[doc.ID] = doc
}
return nil
}
func (f *fakeMediaStore) UpdateStickerSet(_ context.Context, set domain.StickerSet, docs []domain.Document) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.sets[set.ID]; !ok {
return domain.ErrStickerSetInvalid
}
f.sets[set.ID] = set
if f.docs == nil {
f.docs = map[int64]domain.Document{}
}
for _, doc := range docs {
f.docs[doc.ID] = doc
}
return nil
}
func (f *fakeMediaStore) DeleteStickerSet(_ context.Context, setID int64, creatorUserID int64) error {
f.mu.Lock()
defer f.mu.Unlock()
set, ok := f.sets[setID]
if !ok || set.Deleted || set.CreatorUserID != creatorUserID {
return domain.ErrStickerSetInvalid
}
set.Deleted = true
f.sets[setID] = set
return nil
}
func (f *fakeMediaStore) AdminDeleteStickerSet(_ context.Context, setID int64) error {
f.mu.Lock()
defer f.mu.Unlock()
set, ok := f.sets[setID]
if !ok || set.Deleted {
return domain.ErrStickerSetInvalid
}
set.Deleted = true
f.sets[setID] = set
return nil
}
func (f *fakeMediaStore) GetStickerSetByID(_ context.Context, id int64) (domain.StickerSet, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
s, ok := f.sets[id]
if ok && s.Deleted {
return domain.StickerSet{}, false, nil
}
return s, ok, nil
}
func (f *fakeMediaStore) GetStickerSetByShortName(_ context.Context, name string) (domain.StickerSet, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
for _, s := range f.sets {
if strings.EqualFold(s.ShortName, name) && !s.Deleted {
return s, true, nil
}
}
return domain.StickerSet{}, false, nil
}
func (f *fakeMediaStore) GetStickerSetBySystemKey(_ context.Context, key string) (domain.StickerSet, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
for _, s := range f.sets {
if s.SystemKey == key {
return s, true, nil
}
}
return domain.StickerSet{}, false, nil
}
func (f *fakeMediaStore) ListStickerSets(_ context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []domain.StickerSet
for _, s := range f.sets {
if s.Kind == kind && !s.Deleted {
out = append(out, s)
}
}
return out, nil
}
func (f *fakeMediaStore) ListStickerSetsByCreator(_ context.Context, creatorUserID int64, offsetID int64, limit int) ([]domain.StickerSet, int, error) {
f.mu.Lock()
defer f.mu.Unlock()
var all []domain.StickerSet
for _, s := range f.sets {
if s.CreatorUserID == creatorUserID && !s.Deleted {
s.Creator = true
all = append(all, s)
}
}
sort.Slice(all, func(i, j int) bool { return all[i].ID > all[j].ID })
total := len(all)
if offsetID != 0 {
filtered := all[:0]
for _, s := range all {
if s.ID < offsetID {
filtered = append(filtered, s)
}
}
all = filtered
}
if limit > 0 && len(all) > limit {
all = all[:limit]
}
return append([]domain.StickerSet(nil), all...), total, nil
}
func (f *fakeMediaStore) StickerSetShortNameAvailable(_ context.Context, shortName string) (bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
for _, s := range f.sets {
if s.ShortName != "" && strings.EqualFold(s.ShortName, shortName) && !s.Deleted {
return false, nil
}
}
return true, nil
}
func (f *fakeMediaStore) CountStickerSets(_ context.Context) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
return len(f.sets), nil
}
func (f *fakeMediaStore) PutAvailableReaction(_ context.Context, r domain.AvailableReaction) error {
f.mu.Lock()
defer f.mu.Unlock()
for i, existing := range f.reactions {
if existing.Reaction == r.Reaction {
f.reactions[i] = r
return nil
}
}
f.reactions = append(f.reactions, r)
return nil
}
func (f *fakeMediaStore) ListAvailableReactions(_ context.Context) ([]domain.AvailableReaction, error) {
f.mu.Lock()
defer f.mu.Unlock()
return append([]domain.AvailableReaction(nil), f.reactions...), nil
}
func (f *fakeMediaStore) CountAvailableReactions(_ context.Context) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
return len(f.reactions), nil
}
func (f *fakeMediaStore) AddProfilePhotoKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoID int64, date int) error {
f.mu.Lock()
defer f.mu.Unlock()
key := fakeProfilePhotoKey(ownerType, ownerID, kind)
existing := f.profilePhotos[key]
ref := domain.ProfilePhotoRef{PhotoID: photoID}
if p, ok := f.photos[photoID]; ok {
ref.DCID = p.DCID
ref.Stripped = domain.StrippedFromSizes(p.Sizes)
ref.HasVideo = domain.PhotoHasVideo(p.Sizes)
}
if existing.PhotoID != photoID {
f.profilePhotos[key] = ref
}
return nil
}
func (f *fakeMediaStore) CurrentProfilePhotoKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) (int64, bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, ownerID, kind)]
if !ok {
return 0, false, nil
}
return ref.PhotoID, true, nil
}
func (f *fakeMediaStore) CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ids []int64) (map[int64]domain.ProfilePhotoRef, error) {
return f.CurrentProfilePhotosKind(ctx, ownerType, ids, domain.ProfilePhotoKindProfile)
}
func (f *fakeMediaStore) CurrentProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ids []int64, kind domain.ProfilePhotoKind) (map[int64]domain.ProfilePhotoRef, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := make(map[int64]domain.ProfilePhotoRef, len(ids))
for _, id := range ids {
if ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, id, kind)]; ok {
out[id] = ref
}
}
return out, nil
}
func (f *fakeMediaStore) ListProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, offset, limit int, maxID int64) ([]int64, int, error) {
f.mu.Lock()
defer f.mu.Unlock()
var ids []int64
if ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, ownerID, kind)]; ok {
ids = append(ids, ref.PhotoID)
}
return ids, len(ids), nil
}
func (f *fakeMediaStore) ListProfilePhotoDetailsKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, offset, limit int, maxID int64) ([]domain.Photo, int, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []domain.Photo
if ref, ok := f.profilePhotos[fakeProfilePhotoKey(ownerType, ownerID, kind)]; ok {
if p, ok := f.photos[ref.PhotoID]; ok {
out = append(out, p)
}
}
return out, len(out), nil
}
func (f *fakeMediaStore) DeleteProfilePhotos(_ context.Context, ownerType domain.PeerType, ownerID int64, photoIDs []int64) ([]int64, error) {
f.mu.Lock()
defer f.mu.Unlock()
var deleted []int64
key := fakeProfilePhotoKey(ownerType, ownerID, domain.ProfilePhotoKindProfile)
if ref, ok := f.profilePhotos[key]; ok {
for _, id := range photoIDs {
if id == ref.PhotoID {
deleted = append(deleted, id)
}
}
}
if len(deleted) > 0 {
delete(f.profilePhotos, key)
}
return deleted, nil
}
func (f *fakeMediaStore) DeleteProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoIDs []int64) ([]int64, error) {
f.mu.Lock()
defer f.mu.Unlock()
var deleted []int64
key := fakeProfilePhotoKey(ownerType, ownerID, kind)
if ref, ok := f.profilePhotos[key]; ok {
for _, id := range photoIDs {
if id == ref.PhotoID {
deleted = append(deleted, id)
}
}
}
if len(deleted) > 0 {
delete(f.profilePhotos, key)
}
return deleted, nil
}
func (f *fakeMediaStore) WithTx(_ context.Context, fn func(ctx context.Context, txMedia store.MediaStore) error) error {
return fn(context.Background(), f)
}
func TestSeedMediaRepairsPartialReactionBlobs(t *testing.T) {
seedDir := t.TempDir()
if err := os.MkdirAll(filepath.Join(seedDir, "telegram_reactions_export", "global_json"), 0o755); err != nil {
t.Fatal(err)
}
reactionsDir := filepath.Join(seedDir, "telegram_reactions_export", "reactions")
if err := os.MkdirAll(reactionsDir, 0o755); err != nil {
t.Fatal(err)
}
raw := `{"result":{"reactions":[{"reaction":"👍","title":"Like","static_icon":{"id":1111111,"access_hash":1,"file_reference":"","date":"2026-06-03T00:00:00Z","mime_type":"image/webp","size":4,"attributes":[],"thumbs":[]},"select_animation":{"id":2222222,"access_hash":2,"file_reference":"","date":"2026-06-03T00:00:00Z","mime_type":"application/x-tgsticker","size":4,"attributes":[],"thumbs":[]}}]}}`
if err := os.WriteFile(filepath.Join(seedDir, "telegram_reactions_export", "global_json", "available_reactions_raw.json"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(reactionsDir, "reaction_thumbs_up_sign_static_icon_Like_1111111.webp"), []byte("webp"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(reactionsDir, "reaction_thumbs_up_sign_static_icon_Like_1111111_thumb1_PhotoSize_types_72x72.jpg"), []byte("jpeg"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(reactionsDir, "reaction_select_2222222.tgs"), []byte("tgs!"), 0o644); err != nil {
t.Fatal(err)
}
media := newFakeMediaStore()
local, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
blobs := &countingBlobBackend{BlobBackend: local}
svc := NewService(media, blobs, 2)
if stats, err := svc.SeedMedia(context.Background(), seedDir, 0); err != nil {
t.Fatalf("initial seed: %v", err)
} else if stats.Reactions != 1 || stats.Blobs != 2 {
t.Fatalf("initial stats = %+v, want one reaction and two document blobs", stats)
}
chunk, ok, err := svc.GetFile(context.Background(), domain.FileDownloadRequest{LocationKey: "doc:2222222", Offset: 0, Limit: 4})
if err != nil || !ok {
t.Fatalf("prewarmed getfile ok=%v err=%v", ok, err)
}
if string(chunk.Bytes) != "tgs!" {
t.Fatalf("prewarmed chunk = %q, want tgs!", chunk.Bytes)
}
if blobs.getRangeCalls != 0 {
t.Fatalf("seeded small blob should be served from byte cache, GetRange calls = %d", blobs.getRangeCalls)
}
media.mu.Lock()
delete(media.blobs, "doc:2222222")
media.mu.Unlock()
stats, err := svc.SeedMedia(context.Background(), seedDir, 0)
if err != nil {
t.Fatalf("repair seed: %v", err)
}
if stats.Reactions != 1 || stats.Blobs != 2 || stats.Skipped {
t.Fatalf("repair stats = %+v, want two revalidated main document blobs", stats)
}
if _, ok, _ := media.GetFileBlob(context.Background(), "doc:2222222"); !ok {
t.Fatal("missing reaction blob was not repaired")
}
if reactions, _ := media.ListAvailableReactions(context.Background()); len(reactions) != 1 {
t.Fatalf("reaction upsert duplicated rows: got %d", len(reactions))
}
}
func TestSeedStatusPackTGSWithoutExportedThumbUsesBundledPreview(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
const sourceID int64 = 5247133031235329609
writeStatusPackWithoutThumbSeed(t, seedDir, sourceID, 17)
media := newFakeMediaStore()
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
stats, err := svc.SeedMedia(ctx, seedDir, 0)
if err != nil {
t.Fatalf("seed media: %v", err)
}
if stats.StickerSets != 1 || stats.Documents != 1 || stats.Blobs != 2 || stats.Skipped {
t.Fatalf("stats = %+v, want one set, one doc, main blob plus bundled preview", stats)
}
set, ok, err := media.GetStickerSetByShortName(ctx, "StatusPack")
if err != nil || !ok {
t.Fatalf("StatusPack ok=%v err=%v", ok, err)
}
doc, ok, err := media.GetDocument(ctx, set.DocumentIDs[0])
if err != nil || !ok {
t.Fatalf("StatusPack document ok=%v err=%v", ok, err)
}
if !seedDocumentHasAttribute(doc.Attributes, domain.DocAttrCustomEmoji) {
t.Fatalf("document attributes = %+v, want custom emoji", doc.Attributes)
}
thumb, ok := findCachedThumb(doc.Thumbs)
if !ok {
t.Fatalf("document thumbs = %+v, want bundled cached preview", doc.Thumbs)
}
want, ok := seedBundledDocumentPreview(sourceID)
if !ok {
t.Fatal("bundled StatusPack preview missing")
}
if thumb.Type != seedBundledDocumentThumbType || thumb.W != 128 || thumb.H != 128 || !bytes.Equal(thumb.Bytes, want) {
t.Fatalf("bundled thumb = %+v, want visible 128x128 cached %q preview", thumb, seedBundledDocumentThumbType)
}
blob, ok, err := media.GetFileBlob(ctx, fmt.Sprintf("doc:%d:%s", doc.ID, thumb.Type))
if err != nil || !ok {
t.Fatalf("bundled thumb blob ok=%v err=%v", ok, err)
}
if blob.MimeType != "image/png" {
t.Fatalf("bundled thumb blob mime = %q, want image/png", blob.MimeType)
}
}
func TestBundledStatusPackPreviewsAreVisibleTransparentPNGs(t *testing.T) {
if len(seedBundledDocumentPreviews) != 11 {
t.Fatalf("bundled StatusPack previews = %d, want 11", len(seedBundledDocumentPreviews))
}
for documentID, data := range seedBundledDocumentPreviews {
img, err := png.Decode(bytes.NewReader(data))
if err != nil {
t.Fatalf("decode bundled preview %d: %v", documentID, err)
}
if bounds := img.Bounds(); bounds.Dx() != 128 || bounds.Dy() != 128 {
t.Fatalf("bundled preview %d bounds = %v, want 128x128", documentID, bounds)
}
visible := false
transparent := false
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
_, _, _, alpha := img.At(x, y).RGBA()
visible = visible || alpha != 0
transparent = transparent || alpha != 0xffff
}
}
if !visible || !transparent {
t.Fatalf("bundled preview %d visible=%v transparent=%v", documentID, visible, transparent)
}
}
}
func TestSeedMediaDoesNotInventPreviewForUnknownTGS(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
const sourceID int64 = 5555555
const setHash = 23
writeStatusPackWithoutThumbSeed(t, seedDir, sourceID, setHash)
media := newFakeMediaStore()
if err := media.PutDocument(ctx, domain.Document{
ID: sourceID,
MimeType: "application/x-tgsticker",
Attributes: []domain.DocumentAttribute{{
Kind: domain.DocAttrCustomEmoji,
Alt: "\U0001f44b",
TextColor: true,
}},
}); err != nil {
t.Fatalf("put stale document: %v", err)
}
if err := media.PutStickerSet(ctx, domain.StickerSet{
ID: 773947703670341676,
AccessHash: 1,
ShortName: "StatusPack",
Title: "Status Pack",
Hash: setHash,
Kind: domain.StickerSetKindEmoji,
Emojis: true,
DocumentIDs: []int64{
sourceID,
},
}); err != nil {
t.Fatalf("put stale sticker set: %v", err)
}
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
stats, err := svc.SeedMedia(ctx, seedDir, 0)
if err != nil {
t.Fatalf("repair seed: %v", err)
}
if stats.StickerSets != 1 || stats.Documents != 1 || stats.Blobs != 1 || stats.Skipped {
t.Fatalf("reimport stats = %+v, want main document blob only", stats)
}
doc, ok, err := media.GetDocument(ctx, sourceID)
if err != nil || !ok {
t.Fatalf("repaired document ok=%v err=%v", ok, err)
}
if len(doc.Thumbs) != 0 {
t.Fatalf("document thumbs = %+v, want no invented preview for unknown TGS", doc.Thumbs)
}
}
func TestSeedMediaSkipsUnchangedEffectsDocuments(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
const sourceID int64 = 6666666
writeEffectsSeed(t, seedDir, sourceID)
media := newFakeMediaStore()
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
first, err := svc.SeedMedia(ctx, seedDir, 0)
if err != nil {
t.Fatalf("first seed: %v", err)
}
if first.Effects != 1 || first.Documents != 1 || first.Blobs != 1 {
t.Fatalf("first stats = %+v, want one imported effect document with its main blob", first)
}
second, err := svc.SeedMedia(ctx, seedDir, 0)
if err != nil {
t.Fatalf("second seed: %v", err)
}
if second.Effects != 1 || second.Documents != 0 || second.Blobs != 0 {
t.Fatalf("second stats = %+v, want effects catalog loaded without document/blob import", second)
}
delete(media.blobs, fmt.Sprintf("doc:%d", sourceID))
repaired, err := svc.SeedMedia(ctx, seedDir, 0)
if err != nil {
t.Fatalf("repair seed: %v", err)
}
if repaired.Effects != 1 || repaired.Documents != 1 || repaired.Blobs != 1 {
t.Fatalf("repair stats = %+v, want missing main blob repaired without rewriting intact preview", repaired)
}
}
func TestSeedEffectsDoesNotDowngradeSharedStickerPreview(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
const sourceID int64 = 7777777
realThumb := writeStatusPackWithThumbSeed(t, seedDir, sourceID, 29)
writeEffectsSeed(t, seedDir, sourceID)
media := newFakeMediaStore()
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
first, err := svc.SeedMedia(ctx, seedDir, 0)
if err != nil {
t.Fatalf("first seed: %v", err)
}
if first.StickerSets != 1 || first.Effects != 1 {
t.Fatalf("first stats = %+v, want shared sticker and effect catalogs", first)
}
doc, ok, err := media.GetDocument(ctx, sourceID)
if err != nil || !ok {
t.Fatalf("shared document ok=%v err=%v", ok, err)
}
thumb, ok := findCachedThumb(doc.Thumbs)
if !ok {
t.Fatalf("shared document thumbs = %+v, want real cached preview", doc.Thumbs)
}
if thumb.W != 128 || thumb.H != 128 || !bytes.Equal(thumb.Bytes, realThumb) {
t.Fatalf("shared preview = %+v, want original 128x128 catalog thumbnail", thumb)
}
blob, ok, err := media.GetFileBlob(ctx, fmt.Sprintf("doc:%d:m", sourceID))
if err != nil || !ok {
t.Fatalf("shared preview blob ok=%v err=%v", ok, err)
}
if blob.MimeType != "image/jpeg" || blob.Size != int64(len(realThumb)) {
t.Fatalf("shared preview blob = %+v, want real JPEG metadata", blob)
}
chunk, ok, err := svc.GetFile(ctx, domain.FileDownloadRequest{LocationKey: fmt.Sprintf("doc:%d:m", sourceID), Limit: 1024})
if err != nil || !ok {
t.Fatalf("get shared preview ok=%v err=%v", ok, err)
}
if !bytes.Equal(chunk.Bytes, realThumb) {
t.Fatalf("downloaded shared preview = %x, want %x", chunk.Bytes, realThumb)
}
second, err := svc.SeedMedia(ctx, seedDir, 0)
if err != nil {
t.Fatalf("second seed: %v", err)
}
if second.Documents != 0 || second.Blobs != 0 {
t.Fatalf("second stats = %+v, want shared rich preview to satisfy effects readiness", second)
}
}
func TestSeedMediaFromRealExport(t *testing.T) {
seedDir := os.Getenv("TELESRV_REAL_STICKER_SEED_DIR")
if seedDir == "" {
t.Skip("TELESRV_REAL_STICKER_SEED_DIR not set")
}
if _, err := os.Stat(seedDir); err != nil {
t.Skipf("seed dir %s not present: %v", seedDir, err)
}
media := newFakeMediaStore()
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
maxRegularSets := 2
if raw := os.Getenv("TELESRV_REAL_STICKER_SEED_MAX_SETS"); raw != "" {
n, err := strconv.Atoi(raw)
if err != nil {
t.Fatalf("parse TELESRV_REAL_STICKER_SEED_MAX_SETS: %v", err)
}
maxRegularSets = n
}
stats, err := svc.SeedMedia(context.Background(), seedDir, maxRegularSets)
if err != nil {
t.Fatalf("seed media: %v", err)
}
t.Logf("seed stats: reactions=%d sets=%d docs=%d blobs=%d", stats.Reactions, stats.StickerSets, stats.Documents, stats.Blobs)
if stats.Reactions == 0 {
t.Error("expected reactions imported")
}
if stats.StickerSets == 0 {
t.Error("expected sticker sets imported")
}
if stats.Documents == 0 {
t.Error("expected documents imported")
}
if stats.Blobs == 0 {
t.Error("expected blobs imported")
}
// reaction 引用的文档应能被解析回真实 document带 sticker 属性 + 主体 blob
reactions, _ := media.ListAvailableReactions(context.Background())
if len(reactions) == 0 {
t.Fatal("no reactions stored")
}
first := reactions[0]
if first.Reaction == "" {
t.Error("reaction emoticon empty")
}
if first.StaticIconID == 0 || first.SelectAnimationID == 0 {
t.Error("reaction missing document ids")
}
if d, ok, _ := media.GetDocument(context.Background(), first.SelectAnimationID); !ok {
t.Error("reaction select animation document missing")
} else {
if d.DCID != 2 {
t.Errorf("document dc_id not rewritten: %d", d.DCID)
}
if _, ok, _ := media.GetFileBlob(context.Background(), blobKeyDoc(d.ID)); !ok {
t.Errorf("reaction document %d main blob missing", d.ID)
}
}
// 一个常规贴纸集应有 documents 且能按 short_name 解析。
for _, s := range media.sets {
for _, thumb := range s.Thumbs {
if thumb.Downloadable() {
t.Fatalf("sticker set %s exposes downloadable cover thumb %q without a serviceable blob", s.ShortName, thumb.Type)
}
}
}
var sample domain.StickerSet
for _, s := range media.sets {
if s.Kind == domain.StickerSetKindStickers && len(s.DocumentIDs) > 0 {
sample = s
break
}
}
if sample.ID == 0 {
t.Fatal("no regular sticker set with documents imported")
}
if got, ok, _ := media.GetStickerSetByShortName(context.Background(), sample.ShortName); !ok || got.ID != sample.ID {
t.Error("sticker set not resolvable by short name")
}
if doc, ok, _ := media.GetDocument(context.Background(), sample.DocumentIDs[0]); !ok {
t.Fatalf("sample sticker document %d missing", sample.DocumentIDs[0])
} else {
thumb, ok := findCachedThumb(doc.Thumbs)
if !ok {
t.Fatalf("sample sticker document thumbs are not inline cached: %+v", doc.Thumbs)
}
blob, ok, err := media.GetFileBlob(context.Background(), blobKeyDoc(doc.ID)+":"+thumb.Type)
if err != nil || !ok {
t.Fatalf("sample sticker thumb blob ok=%v err=%v", ok, err)
}
if want := seedThumbMimeType(thumb.Bytes); blob.MimeType != want {
t.Fatalf("sample sticker thumb mime = %q, want %q", blob.MimeType, want)
}
if !hasPathThumb(doc.Thumbs) {
t.Logf("sample sticker document has no exported PhotoPathSize placeholder; cached preview is present: %+v", doc.Thumbs)
}
}
}
func writeStatusPackWithoutThumbSeed(t *testing.T, seedDir string, sourceID int64, setHash int) {
t.Helper()
setDir := filepath.Join(seedDir, "telegram_emoji_export", "StatusPack_773947703670341676")
stickersDir := filepath.Join(setDir, "stickers")
if err := os.MkdirAll(stickersDir, 0o755); err != nil {
t.Fatal(err)
}
raw := fmt.Sprintf(`{"result":{"set":{"id":773947703670341676,"access_hash":1,"title":"Status Pack","short_name":"StatusPack","count":1,"hash":%d,"emojis":true,"packs":[{"emoticon":"👋","documents":[%d]}]},"packs":[{"emoticon":"👋","documents":[%d]}],"documents":[{"id":%d,"access_hash":2,"file_reference":"","date":"2026-06-29T00:00:00Z","mime_type":"application/x-tgsticker","size":4,"dc_id":4,"attributes":[{"_":"DocumentAttributeImageSize","w":512,"h":512},{"_":"DocumentAttributeCustomEmoji","alt":"👋","text_color":true,"stickerset":{"id":773947703670341676,"access_hash":1}},{"_":"DocumentAttributeFilename","file_name":"AnimatedSticker.tgs"}],"thumbs":[]}]}}`, setHash, sourceID, sourceID, sourceID)
if err := os.WriteFile(filepath.Join(setDir, "set_info.json"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(stickersDir, fmt.Sprintf("status_%d.tgs", sourceID)), []byte("tgs!"), 0o644); err != nil {
t.Fatal(err)
}
}
func writeStatusPackWithThumbSeed(t *testing.T, seedDir string, sourceID int64, setHash int) []byte {
t.Helper()
setDir := filepath.Join(seedDir, "telegram_emoji_export", "StatusPack_773947703670341676")
stickersDir := filepath.Join(setDir, "stickers")
if err := os.MkdirAll(stickersDir, 0o755); err != nil {
t.Fatal(err)
}
realThumb := []byte{0xff, 0xd8, 0xff, 0xdb, 0, 4, 0xff, 0xd9}
raw := fmt.Sprintf(`{"result":{"set":{"id":773947703670341676,"access_hash":1,"title":"Status Pack","short_name":"StatusPack","count":1,"hash":%d,"emojis":true,"packs":[{"emoticon":"👋","documents":[%d]}]},"packs":[{"emoticon":"👋","documents":[%d]}],"documents":[{"id":%d,"access_hash":2,"file_reference":"","date":"2026-06-29T00:00:00Z","mime_type":"application/x-tgsticker","size":4,"dc_id":4,"attributes":[{"_":"DocumentAttributeImageSize","w":512,"h":512},{"_":"DocumentAttributeCustomEmoji","alt":"👋","text_color":true,"stickerset":{"id":773947703670341676,"access_hash":1}},{"_":"DocumentAttributeFilename","file_name":"AnimatedSticker.tgs"}],"thumbs":[{"_":"PhotoPathSize","type":"j","bytes":"01"},{"_":"PhotoSize","type":"m","w":128,"h":128,"size":%d}]}]}}`, setHash, sourceID, sourceID, sourceID, len(realThumb))
if err := os.WriteFile(filepath.Join(setDir, "set_info.json"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(stickersDir, fmt.Sprintf("status_%d.tgs", sourceID)), []byte("tgs!"), 0o644); err != nil {
t.Fatal(err)
}
thumbName := fmt.Sprintf("status_%d_thumb1_PhotoSize_typem_128x128.jpg", sourceID)
if err := os.WriteFile(filepath.Join(stickersDir, thumbName), realThumb, 0o644); err != nil {
t.Fatal(err)
}
return realThumb
}
func writeEffectsSeed(t *testing.T, seedDir string, sourceID int64) {
t.Helper()
docsDir := filepath.Join(seedDir, "telegram_effects_export", "documents")
if err := os.MkdirAll(docsDir, 0o755); err != nil {
t.Fatal(err)
}
raw := fmt.Sprintf(`{"result":{"effects":[{"id":77,"emoticon":"🔥","effect_sticker_id":%d}],"documents":[{"id":%d,"access_hash":2,"file_reference":"","date":"2026-06-29T00:00:00Z","mime_type":"application/x-tgsticker","size":4,"dc_id":4,"attributes":[{"_":"DocumentAttributeImageSize","w":512,"h":512},{"_":"DocumentAttributeFilename","file_name":"effect.tgs"}],"thumbs":[]}]}}`, sourceID, sourceID)
if err := os.WriteFile(filepath.Join(seedDir, "telegram_effects_export", "effects.json"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(docsDir, fmt.Sprintf("effect_%d.tgs", sourceID)), []byte("tgs!"), 0o644); err != nil {
t.Fatal(err)
}
}
func TestSeedDocumentIDsAreImportedVerbatim(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
const sourceID int64 = 5382305375846410902
writeStatusPackWithoutThumbSeed(t, seedDir, sourceID, 17)
media := newFakeMediaStore()
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
if _, err := svc.SeedMedia(ctx, seedDir, 0); err != nil {
t.Fatalf("seed media: %v", err)
}
set, ok, err := media.GetStickerSetByShortName(ctx, "StatusPack")
if err != nil || !ok {
t.Fatalf("StatusPack ok=%v err=%v", ok, err)
}
if len(set.DocumentIDs) != 1 || set.DocumentIDs[0] != sourceID {
t.Fatalf("set document ids = %+v, want [%d]", set.DocumentIDs, sourceID)
}
doc, ok, err := media.GetDocument(ctx, sourceID)
if err != nil || !ok {
t.Fatalf("GetDocument(%d) ok=%v err=%v", sourceID, ok, err)
}
if doc.ID != sourceID {
t.Fatalf("document id = %d, want %d", doc.ID, sourceID)
}
if _, ok, err := media.GetFileBlob(ctx, fmt.Sprintf("doc:%d", sourceID)); err != nil || !ok {
t.Fatalf("main blob for raw id ok=%v err=%v", ok, err)
}
}
func TestSeedMediaUsesInputSetTypeForSystemSets(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
cases := []struct {
name string
category string
dirName string
inputSetType string
shortName string
id int64
docID int64
emojis bool
wantSystem string
}{
{
name: "default topic icons from emoji export",
category: "telegram_emoji_export",
dirName: "Topics_7173162320003082",
inputSetType: "*tg.InputStickerSetEmojiDefaultTopicIcons",
shortName: "Topics",
id: 7173162320003082,
docID: 5312536423851630001,
emojis: true,
wantSystem: domain.StickerSetSystemKeyEmojiDefaultTopicIcons,
},
{
name: "premium gifts from fetched sticker export",
category: "telegram_stickers_export",
dirName: "PremiumGifts_328917524764688479",
inputSetType: "InputStickerSetPremiumGifts",
shortName: "PremiumGifts",
id: 328917524764688479,
docID: 5710643921839718407,
wantSystem: domain.StickerSetSystemKeyPremiumGifts,
},
{
name: "ton gifts from fetched sticker export",
category: "telegram_stickers_export",
dirName: "TonGifts_9000000000000001",
inputSetType: "*tg.InputStickerSetTonGifts",
shortName: "TonGifts",
id: 9000000000000001,
docID: 9000000000000101,
wantSystem: domain.StickerSetSystemKeyTonGifts,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := filepath.Join(seedDir, tc.category, tc.dirName)
writeInputSetTypeSeed(t, dir, tc.inputSetType, tc.shortName, tc.id, tc.docID, tc.emojis)
media := newFakeMediaStore()
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
if _, err := svc.SeedMedia(ctx, seedDir, 0); err != nil {
t.Fatalf("seed media: %v", err)
}
set, found, err := media.GetStickerSetBySystemKey(ctx, tc.wantSystem)
if err != nil || !found {
t.Fatalf("GetStickerSetBySystemKey(%q) found=%v err=%v", tc.wantSystem, found, err)
}
if set.Kind != domain.StickerSetKindSystem || set.SystemKey != tc.wantSystem {
t.Fatalf("set meta = %+v, want system key %q", set, tc.wantSystem)
}
if set.ID != tc.id || set.ShortName != tc.shortName || len(set.DocumentIDs) != 1 || set.DocumentIDs[0] != tc.docID {
t.Fatalf("set = %+v, want id=%d short=%s doc=%d", set, tc.id, tc.shortName, tc.docID)
}
resolved, docs, found, err := svc.ResolveStickerSet(ctx, domain.StickerSetRef{
Kind: domain.StickerSetRefBySystem,
SystemKey: tc.wantSystem,
})
if err != nil || !found {
t.Fatalf("ResolveStickerSet(%q) found=%v err=%v", tc.wantSystem, found, err)
}
if resolved.ID != tc.id || len(docs) != 1 || docs[0].ID != tc.docID {
t.Fatalf("resolved = set %d docs %+v, want set %d doc %d", resolved.ID, docs, tc.id, tc.docID)
}
})
}
}
func TestSeedMediaReclassifiesExistingSystemSetWhenHashUnchanged(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
const (
setID int64 = 7173162320003082
docID int64 = 5312536423851630001
shortName = "Topics"
)
writeInputSetTypeSeed(t,
filepath.Join(seedDir, "telegram_emoji_export", "Topics_7173162320003082"),
"*tg.InputStickerSetEmojiDefaultTopicIcons",
shortName,
setID,
docID,
true,
)
media := newFakeMediaStore()
if err := media.PutStickerSet(ctx, domain.StickerSet{
ID: setID,
ShortName: shortName,
Hash: 17,
Kind: domain.StickerSetKindEmoji,
}); err != nil {
t.Fatalf("preload existing set: %v", err)
}
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
svc := NewService(media, blobs, 2)
if _, err := svc.SeedMedia(ctx, seedDir, 0); err != nil {
t.Fatalf("seed media: %v", err)
}
set, found, err := media.GetStickerSetBySystemKey(ctx, domain.StickerSetSystemKeyEmojiDefaultTopicIcons)
if err != nil || !found {
t.Fatalf("GetStickerSetBySystemKey(topic_icons) found=%v err=%v", found, err)
}
if set.Kind != domain.StickerSetKindSystem || set.SystemKey != domain.StickerSetSystemKeyEmojiDefaultTopicIcons {
t.Fatalf("set meta = %+v, want reclassified system set", set)
}
if len(set.DocumentIDs) != 1 || set.DocumentIDs[0] != docID {
t.Fatalf("set document ids = %+v, want [%d]", set.DocumentIDs, docID)
}
}
func writeInputSetTypeSeed(t *testing.T, setDir, inputSetType, shortName string, setID, docID int64, emojis bool) {
t.Helper()
stickersDir := filepath.Join(setDir, "stickers")
if err := os.MkdirAll(stickersDir, 0o755); err != nil {
t.Fatal(err)
}
attrType := "DocumentAttributeSticker"
if emojis {
attrType = "DocumentAttributeCustomEmoji"
}
raw := fmt.Sprintf(`{"api_call":"messages.getStickerSet","input_set_type":%q,"result":{"_":"StickerSet","set":{"id":%d,"access_hash":2,"title":%q,"short_name":%q,"count":1,"hash":17,"emojis":%v},"packs":[{"emoticon":"🎁","documents":[%d]}],"documents":[{"id":%d,"access_hash":3,"file_reference":"","date":"2026-07-05T00:00:00Z","mime_type":"application/x-tgsticker","size":4,"dc_id":4,"attributes":[{"_":"DocumentAttributeImageSize","w":512,"h":512},{"_":%q,"alt":"🎁","stickerset":{"id":%d,"access_hash":2}},{"_":"DocumentAttributeFilename","file_name":"AnimatedSticker.tgs"}],"thumbs":[]}]}}`, inputSetType, setID, shortName, shortName, emojis, docID, docID, attrType, setID)
if err := os.WriteFile(filepath.Join(setDir, "set_info.json"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(stickersDir, fmt.Sprintf("system_%d.tgs", docID)), []byte("tgs!"), 0o644); err != nil {
t.Fatal(err)
}
}
func TestSeedStickerSetInstalledFlagNeverMarksViewerState(t *testing.T) {
cases := []struct {
name string
kind domain.StickerSetKind
want bool
}{
{name: "regular stickers", kind: domain.StickerSetKindStickers, want: false},
{name: "custom emoji", kind: domain.StickerSetKindEmoji, want: false},
{name: "masks", kind: domain.StickerSetKindMasks, want: false},
{name: "system resources", kind: domain.StickerSetKindSystem, want: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := seedStickerSetInstalled(tc.kind); got != tc.want {
t.Fatalf("seedStickerSetInstalled(%q) = %v, want %v", tc.kind, got, tc.want)
}
})
}
}
func TestSeedInlineCachedDocumentThumb(t *testing.T) {
input := domain.PhotoSize{Kind: domain.PhotoSizeKindDefault, Type: "m", W: 128, H: 128, Size: 6400}
got := seedInlineCachedDocumentThumb(input, []byte("jpeg"))
if got.Kind != domain.PhotoSizeKindCached {
t.Fatalf("kind = %q, want cached", got.Kind)
}
if got.Size != 0 || string(got.Bytes) != "jpeg" {
t.Fatalf("cached thumb = %+v, want inline bytes without downloadable size", got)
}
large := make([]byte, seedInlineCachedDocumentThumbMaxBytes+1)
if got := seedInlineCachedDocumentThumb(input, large); got.Kind != domain.PhotoSizeKindDefault || got.Size != input.Size || len(got.Bytes) != 0 {
t.Fatalf("large thumb = %+v, want unchanged downloadable thumb", got)
}
}
func TestSeedThumbMimeType(t *testing.T) {
webp := []byte{'R', 'I', 'F', 'F', 0, 0, 0, 0, 'W', 'E', 'B', 'P'}
if got := seedThumbMimeType(webp); got != "image/webp" {
t.Fatalf("webp mime = %q, want image/webp", got)
}
jpeg := []byte{0xFF, 0xD8, 0xFF}
if got := seedThumbMimeType(jpeg); got != "image/jpeg" {
t.Fatalf("jpeg mime = %q, want image/jpeg", got)
}
}
func TestDocumentsNeedInlineCachedThumbsDetectsStaleMime(t *testing.T) {
ctx := context.Background()
media := newFakeMediaStore()
webp := []byte{'R', 'I', 'F', 'F', 0, 0, 0, 0, 'W', 'E', 'B', 'P'}
doc := domain.Document{
ID: 100,
Thumbs: []domain.PhotoSize{
{Kind: domain.PhotoSizeKindCached, Type: "m", Bytes: webp},
},
}
if err := media.PutDocument(ctx, doc); err != nil {
t.Fatalf("put doc: %v", err)
}
if err := media.PutFileBlob(ctx, domain.FileBlob{LocationKey: "doc:100:m", Size: int64(len(webp)), MimeType: "image/jpeg"}); err != nil {
t.Fatalf("put blob: %v", err)
}
svc := NewService(media, nil, 2)
stale, err := svc.documentsNeedInlineCachedThumbs(ctx, []int64{doc.ID})
if err != nil {
t.Fatalf("documentsNeedInlineCachedThumbs: %v", err)
}
if !stale {
t.Fatal("expected stale mime to require repair")
}
if err := media.PutFileBlob(ctx, domain.FileBlob{LocationKey: "doc:100:m", Size: int64(len(webp)), MimeType: "image/webp"}); err != nil {
t.Fatalf("put repaired blob: %v", err)
}
stale, err = svc.documentsNeedInlineCachedThumbs(ctx, []int64{doc.ID})
if err != nil {
t.Fatalf("documentsNeedInlineCachedThumbs after repair: %v", err)
}
if stale {
t.Fatal("repaired mime should not require repair")
}
}
func findCachedThumb(sizes []domain.PhotoSize) (domain.PhotoSize, bool) {
for _, size := range sizes {
if size.Kind == domain.PhotoSizeKindCached && len(size.Bytes) > 0 {
return size, true
}
}
return domain.PhotoSize{}, false
}
func hasPathThumb(sizes []domain.PhotoSize) bool {
for _, size := range sizes {
if size.Kind == domain.PhotoSizeKindPath && len(size.Bytes) > 0 {
return true
}
}
return false
}
func blobKeyDoc(id int64) string {
return "doc:" + itoa(id)
}
func itoa(v int64) string {
if v == 0 {
return "0"
}
neg := v < 0
if neg {
v = -v
}
var buf [20]byte
i := len(buf)
for v > 0 {
i--
buf[i] = byte('0' + v%10)
v /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}