owpengram-server/internal/app/stargifts/animation_test.go

175 lines
8 KiB
Go

package stargifts
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"testing"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
const validGiftLottie = `{"v":"5.7.4","fr":30,"ip":0,"op":60,"w":512,"h":512,"layers":[{"ty":4,"nm":"gift"}],"assets":[]}`
func TestPrepareAnimationNormalizesLottieAndTGS(t *testing.T) {
fromJSON, err := prepareAnimation("gift.lottie", []byte(" \n"+validGiftLottie+"\n"))
if err != nil {
t.Fatalf("prepare lottie: %v", err)
}
if fromJSON.SourceFormat != domain.StarGiftAnimationLottie || len(fromJSON.TGS) == 0 || fromJSON.Width != 512 || fromJSON.Height != 512 {
t.Fatalf("prepared lottie = %+v", fromJSON)
}
fromTGS, err := prepareAnimation("gift.tgs", fromJSON.TGS)
if err != nil {
t.Fatalf("prepare tgs: %v", err)
}
if fromTGS.SourceFormat != domain.StarGiftAnimationTGS || string(fromTGS.JSON) != string(fromJSON.JSON) || hex.EncodeToString(fromTGS.SHA256) != hex.EncodeToString(fromJSON.SHA256) {
t.Fatalf("tgs round trip differs: json=%v hash=%x/%x", string(fromTGS.JSON) == string(fromJSON.JSON), fromTGS.SHA256, fromJSON.SHA256)
}
}
func TestPrepareAnimationRejectsExternalAssetAndExpression(t *testing.T) {
for name, raw := range map[string]string{
"external": `{"v":"5.7","fr":30,"ip":0,"op":30,"w":512,"h":512,"layers":[{}],"assets":[{"p":"https://example.test/x.png"}]}`,
"expression": `{"v":"5.7","fr":30,"ip":0,"op":30,"w":512,"h":512,"layers":[{"ks":{"o":{"x":"time*10"}}}]}`,
"wrong-size": `{"v":"5.7","fr":30,"ip":0,"op":30,"w":256,"h":256,"layers":[{}]}`,
"frame-rate": `{"v":"5.7","fr":121,"ip":0,"op":30,"w":512,"h":512,"layers":[{}]}`,
"duration": `{"v":"5.7","fr":30,"ip":0,"op":901,"w":512,"h":512,"layers":[{}]}`,
} {
t.Run(name, func(t *testing.T) {
if _, err := prepareAnimation("gift.json", []byte(raw)); !errors.Is(err, domain.ErrStarGiftFileInvalid) {
t.Fatalf("err=%v, want ErrStarGiftFileInvalid", err)
}
})
}
}
type testGiftBlob struct{ data map[string][]byte }
func (b *testGiftBlob) Name() string { return "localfs" }
func (b *testGiftBlob) Put(_ context.Context, data []byte) (string, error) {
sum := sha256.Sum256(data)
key := hex.EncodeToString(sum[:])
b.data[key] = append([]byte(nil), data...)
return key, nil
}
func (b *testGiftBlob) Get(_ context.Context, key string) ([]byte, error) {
return append([]byte(nil), b.data[key]...), nil
}
func TestCreateCatalogRevisionPreservesHistoricalRevision(t *testing.T) {
ctx := context.Background()
store := memory.NewStarGiftStore()
svc := NewService(store, &testGiftBlob{data: map[string][]byte{}}, 2)
animation, err := svc.PrepareAnimation("gift.json", []byte(validGiftLottie))
if err != nil {
t.Fatal(err)
}
first, err := svc.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
Stars: 50, ConvertStars: 25, Enabled: true, SortOrder: 1, Title: "Telegram Pin", Animation: animation,
})
if err != nil {
t.Fatalf("create first: %v", err)
}
second, err := svc.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
GiftID: first.Gift.ID, Stars: 80, ConvertStars: 40, Enabled: true, SortOrder: 1, Title: "Second", Animation: animation,
})
if err != nil {
t.Fatalf("create second: %v", err)
}
current, found, _ := svc.GiftByID(ctx, first.Gift.ID)
if !found || current.RevisionID != second.Gift.RevisionID || current.Stars != 80 {
t.Fatalf("current=%+v found=%v", current, found)
}
historical, found, _ := svc.GiftRevisionByID(ctx, first.Gift.RevisionID)
if !found || historical.Stars != 50 || historical.Title != "OwpenGram Pin" {
t.Fatalf("historical=%+v found=%v", historical, found)
}
if _, err := svc.SetCatalogEnabled(ctx, first.Gift.ID+999, false); !errors.Is(err, domain.ErrStarGiftNotFound) {
t.Fatalf("disable missing err=%v, want ErrStarGiftNotFound", err)
}
}
func TestCreateCatalogBundleRejectsMismatchedOfficialProvenance(t *testing.T) {
ctx := context.Background()
store := memory.NewStarGiftStore()
svc := NewService(store, &testGiftBlob{data: map[string][]byte{}}, 2)
animation, err := svc.PrepareAnimation("gift.json", []byte(validGiftLottie))
if err != nil {
t.Fatal(err)
}
hash := make([]byte, sha256.Size)
_, err = svc.CreateCatalogBundle(ctx, domain.StarGiftCatalogBundleWrite{
Catalog: domain.StarGiftCatalogWrite{
Stars: 50, ConvertStars: 25, Enabled: true, Title: "Official", Animation: animation,
OfficialGiftID: 10, SourceManifestSHA256: hash, OfficialSourceJSON: []byte(`{"id":10}`),
},
Collectible: &domain.StarGiftCollectibleWrite{
OfficialGiftID: 11, SourceManifestSHA256: hash,
},
})
if !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) {
t.Fatalf("mismatched provenance err=%v, want ErrStarGiftCollectibleInvalid", err)
}
}
func TestCreateCatalogBundleMaterializesPublishableCollectibleDocuments(t *testing.T) {
ctx := context.Background()
store := memory.NewStarGiftStore()
svc := NewService(store, &testGiftBlob{data: map[string][]byte{}}, 2)
animation, err := svc.PrepareOfficialAnimation("official.json", []byte(validGiftLottie))
if err != nil {
t.Fatal(err)
}
manifestSHA := make([]byte, sha256.Size)
result, err := svc.CreateCatalogBundle(ctx, domain.StarGiftCatalogBundleWrite{
Catalog: domain.StarGiftCatalogWrite{
Title: "Official", Stars: 50, ConvertStars: 25, Enabled: true, Animation: animation,
Actor: "test", CommandID: "official-catalog", OfficialGiftID: 10,
SourceManifestSHA256: manifestSHA, OfficialSourceJSON: []byte(`{"id":10}`),
},
Collectible: &domain.StarGiftCollectibleWrite{
UpgradeStars: 100, SupplyTotal: 1000, SlugPrefix: "official-10",
Models: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleModel, Name: "Model", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation},
{Kind: domain.StarGiftCollectibleModel, Name: "Model Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation},
},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation},
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 1, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 2, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
},
Actor: "test", CommandID: "official-pool", OfficialGiftID: 10,
SourceManifestSHA256: manifestSHA,
},
})
if err != nil {
t.Fatalf("create official collectible bundle: %v", err)
}
if result.Collectible == nil || len(result.Collectible.Models) != 2 || len(result.Collectible.Patterns) != 2 {
t.Fatalf("collectible result = %+v", result.Collectible)
}
model := result.Collectible.Models[0].Document
pattern := result.Collectible.Patterns[0].Document
if model == nil || !model.IsSticker() || model.IsCustomEmoji() {
t.Fatalf("model document = %+v, want ordinary sticker", model)
}
if pattern == nil || pattern.IsSticker() || !pattern.IsCustomEmoji() || len(pattern.Thumbs) != 1 ||
pattern.Thumbs[0].Kind != domain.PhotoSizeKindPath || len(pattern.Thumbs[0].Bytes) == 0 {
t.Fatalf("pattern document = %+v, want text-color custom emoji with inline path", pattern)
}
if !pattern.Attributes[1].TextColor {
t.Fatalf("pattern render attribute = %+v, want text_color", pattern.Attributes[1])
}
preview, found, err := svc.CollectiblePreviewSample(ctx, result.Catalog.Gift.ID)
if err != nil || !found || len(preview.Models) != 2 || len(preview.Patterns) != 2 || len(preview.Backdrops) != 2 ||
preview.Models[0].Animation == nil || len(preview.Models[0].Animation.JSON) != 0 ||
preview.Patterns[0].Animation == nil || len(preview.Patterns[0].Animation.JSON) != 0 {
t.Fatalf("collectible preview sample = found:%v err:%v value:%+v", found, err, preview)
}
}