feat: sync official star gift lifecycle tooling
Sync telesrv 4c0e2d9 (feat: complete official star gift lifecycle and admin tooling). Public adjustments: skipped private docs/deploy-nginx/README source changes, kept iamxvbaba/td public dependency, replaced local/private sample IP and orange seed label.
This commit is contained in:
parent
f2c2fd0236
commit
14bf7d1e20
92 changed files with 14768 additions and 727 deletions
531
internal/officialgifts/catalog.go
Normal file
531
internal/officialgifts/catalog.go
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
// Package officialgifts reads the local, immutable snapshot produced by cmd/giftfetch.
|
||||
// It never performs network I/O. Selected document bytes are accepted only after their
|
||||
// manifest size and SHA-256 have been verified beneath the configured snapshot root.
|
||||
package officialgifts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const manifestSchema = 2
|
||||
|
||||
var (
|
||||
ErrUnavailable = errors.New("official gifts snapshot is unavailable")
|
||||
ErrInvalid = errors.New("official gifts snapshot is invalid")
|
||||
ErrNotFound = errors.New("official gift not found")
|
||||
)
|
||||
|
||||
type Catalog struct {
|
||||
root string
|
||||
once sync.Once
|
||||
snap *snapshot
|
||||
err error
|
||||
}
|
||||
|
||||
type GiftSummary struct {
|
||||
ID int64
|
||||
Title string
|
||||
Stars int64
|
||||
ConvertStars int64
|
||||
UpgradeStars int64
|
||||
AvailabilityTotal int
|
||||
AvailabilityRemains int
|
||||
AvailabilityResale int64
|
||||
Limited bool
|
||||
SoldOut bool
|
||||
Birthday bool
|
||||
RequirePremium bool
|
||||
LimitedPerUser bool
|
||||
PeerColorAvailable bool
|
||||
Auction bool
|
||||
FirstSaleDate int
|
||||
LastSaleDate int
|
||||
ResellMinStars int64
|
||||
PerUserTotal int
|
||||
PerUserRemains int
|
||||
LockedUntilDate int
|
||||
AuctionSlug string
|
||||
GiftsPerRound int
|
||||
AuctionStartDate int
|
||||
UpgradeVariants int
|
||||
Background *Background
|
||||
ModelCount int
|
||||
PatternCount int
|
||||
BackdropCount int
|
||||
CraftedModelCount int
|
||||
DocumentID int64
|
||||
AnimationValidated bool
|
||||
}
|
||||
|
||||
// CanUpgrade reports whether this snapshot contains the complete immutable
|
||||
// attribute pool and a positive official upgrade price required to mint a
|
||||
// collectible from the regular gift.
|
||||
func (g GiftSummary) CanUpgrade() bool {
|
||||
return g.UpgradeStars > 0 && g.ModelCount > 0 && g.PatternCount > 0 && g.BackdropCount > 0
|
||||
}
|
||||
|
||||
// CanCraft reports whether upgraded collectibles from this gift can reach an
|
||||
// official craft-only model. Craft is not advertised without a valid upgrade
|
||||
// path even if a malformed snapshot were to contain a crafted model.
|
||||
func (g GiftSummary) CanCraft() bool {
|
||||
return g.CanUpgrade() && g.CraftedModelCount > 0
|
||||
}
|
||||
|
||||
type Bundle struct {
|
||||
ManifestSHA256 []byte
|
||||
SourceJSON []byte
|
||||
Gift Gift
|
||||
BaseDocument Document
|
||||
Collectible *CollectibleSet
|
||||
}
|
||||
|
||||
type Gift struct {
|
||||
ID int64
|
||||
Title string
|
||||
Stars int64
|
||||
ConvertStars int64
|
||||
UpgradeStars int64
|
||||
AvailabilityTotal int
|
||||
AvailabilityRemains int
|
||||
Limited bool
|
||||
SoldOut bool
|
||||
Birthday bool
|
||||
RequirePremium bool
|
||||
LimitedPerUser bool
|
||||
PeerColorAvailable bool
|
||||
Auction bool
|
||||
AvailabilityResale int64
|
||||
FirstSaleDate int
|
||||
LastSaleDate int
|
||||
ResellMinStars int64
|
||||
PerUserTotal int
|
||||
PerUserRemains int
|
||||
LockedUntilDate int
|
||||
AuctionSlug string
|
||||
GiftsPerRound int
|
||||
AuctionStartDate int
|
||||
UpgradeVariants int
|
||||
Background *Background
|
||||
DocumentID int64
|
||||
}
|
||||
|
||||
type Background struct {
|
||||
CenterColor int `json:"center_color"`
|
||||
EdgeColor int `json:"edge_color"`
|
||||
TextColor int `json:"text_color"`
|
||||
}
|
||||
|
||||
type CollectibleSet struct {
|
||||
Models []Model
|
||||
Patterns []Pattern
|
||||
Backdrops []Backdrop
|
||||
}
|
||||
|
||||
type Rarity struct {
|
||||
Kind string `json:"kind"`
|
||||
Permille *int `json:"permille,omitempty"`
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Name string
|
||||
DocumentID int64
|
||||
Crafted bool
|
||||
Rarity Rarity
|
||||
Document Document
|
||||
}
|
||||
|
||||
type Pattern struct {
|
||||
Name string
|
||||
DocumentID int64
|
||||
Rarity Rarity
|
||||
Document Document
|
||||
}
|
||||
|
||||
type Backdrop struct {
|
||||
Name string
|
||||
BackdropID int
|
||||
CenterColor int
|
||||
EdgeColor int
|
||||
PatternColor int
|
||||
TextColor int
|
||||
Rarity Rarity
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID int64
|
||||
FileName string
|
||||
Path string
|
||||
Size int64
|
||||
SHA256 string
|
||||
AnimationValidated bool
|
||||
ValidationError string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Schema int `json:"schema"`
|
||||
GiftCount int `json:"gift_count"`
|
||||
Gifts []giftManifest `json:"gifts"`
|
||||
UpgradeAttributeSets []collectibleManifest `json:"upgrade_attribute_sets"`
|
||||
Documents []documentManifest `json:"documents"`
|
||||
}
|
||||
|
||||
type giftManifest struct {
|
||||
Index int `json:"index"`
|
||||
Kind string `json:"kind"`
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Stars int64 `json:"stars"`
|
||||
ConvertStars int64 `json:"convert_stars"`
|
||||
UpgradeStars int64 `json:"upgrade_stars"`
|
||||
Limited bool `json:"limited"`
|
||||
SoldOut bool `json:"sold_out"`
|
||||
Birthday bool `json:"birthday"`
|
||||
RequirePremium bool `json:"require_premium"`
|
||||
LimitedPerUser bool `json:"limited_per_user"`
|
||||
PeerColorAvailable bool `json:"peer_color_available"`
|
||||
Auction bool `json:"auction"`
|
||||
AvailabilityRemains int `json:"availability_remains"`
|
||||
AvailabilityTotal int `json:"availability_total"`
|
||||
AvailabilityResale int64 `json:"availability_resale"`
|
||||
FirstSaleDate int `json:"first_sale_date"`
|
||||
LastSaleDate int `json:"last_sale_date"`
|
||||
ResellMinStars int64 `json:"resell_min_stars"`
|
||||
PerUserTotal int `json:"per_user_total"`
|
||||
PerUserRemains int `json:"per_user_remains"`
|
||||
LockedUntilDate int `json:"locked_until_date"`
|
||||
AuctionSlug string `json:"auction_slug"`
|
||||
GiftsPerRound int `json:"gifts_per_round"`
|
||||
AuctionStartDate int `json:"auction_start_date"`
|
||||
UpgradeVariants int `json:"upgrade_variants"`
|
||||
Background *Background `json:"background"`
|
||||
DocumentIDs []int64 `json:"document_ids"`
|
||||
SourceJSON []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (g *giftManifest) UnmarshalJSON(data []byte) error {
|
||||
type plain giftManifest
|
||||
var value plain
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*g = giftManifest(value)
|
||||
var compact bytes.Buffer
|
||||
if err := json.Compact(&compact, data); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SourceJSON = append([]byte(nil), compact.Bytes()...)
|
||||
return nil
|
||||
}
|
||||
|
||||
type collectibleManifest struct {
|
||||
GiftID64 int64 `json:"gift_id"`
|
||||
AttributeCount int `json:"attribute_count"`
|
||||
Models []modelManifest `json:"models"`
|
||||
Patterns []patternManifest `json:"patterns"`
|
||||
Backdrops []backdropManifest `json:"backdrops"`
|
||||
}
|
||||
|
||||
type modelManifest struct {
|
||||
Name string `json:"name"`
|
||||
DocumentID int64 `json:"document_id"`
|
||||
Crafted bool `json:"crafted"`
|
||||
Rarity Rarity `json:"rarity"`
|
||||
}
|
||||
|
||||
type patternManifest struct {
|
||||
Name string `json:"name"`
|
||||
DocumentID int64 `json:"document_id"`
|
||||
Rarity Rarity `json:"rarity"`
|
||||
}
|
||||
|
||||
type backdropManifest struct {
|
||||
Name string `json:"name"`
|
||||
BackdropID int `json:"backdrop_id"`
|
||||
CenterColor int `json:"center_color"`
|
||||
EdgeColor int `json:"edge_color"`
|
||||
PatternColor int `json:"pattern_color"`
|
||||
TextColor int `json:"text_color"`
|
||||
Rarity Rarity `json:"rarity"`
|
||||
}
|
||||
|
||||
type documentManifest struct {
|
||||
ID64 int64 `json:"id"`
|
||||
FileName string `json:"file_name"`
|
||||
File fileArtifact `json:"file"`
|
||||
AnimationValidated bool `json:"animation_validated"`
|
||||
ValidationError string `json:"validation_error"`
|
||||
}
|
||||
|
||||
type fileArtifact struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
type snapshot struct {
|
||||
manifestSHA []byte
|
||||
gifts map[int64]giftManifest
|
||||
sets map[int64]collectibleManifest
|
||||
documents map[int64]documentManifest
|
||||
ordered []int64
|
||||
}
|
||||
|
||||
func New(root string) *Catalog {
|
||||
return &Catalog{root: strings.TrimSpace(root)}
|
||||
}
|
||||
|
||||
func (c *Catalog) List(ctx context.Context) ([]GiftSummary, error) {
|
||||
snap, err := c.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]GiftSummary, 0, len(snap.ordered))
|
||||
for _, id := range snap.ordered {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gift := snap.gifts[id]
|
||||
doc := snap.documents[gift.DocumentIDs[0]]
|
||||
summary := GiftSummary{
|
||||
ID: id, Title: gift.Title, Stars: gift.Stars, ConvertStars: gift.ConvertStars,
|
||||
UpgradeStars: gift.UpgradeStars, AvailabilityTotal: gift.AvailabilityTotal,
|
||||
AvailabilityRemains: gift.AvailabilityRemains, AvailabilityResale: gift.AvailabilityResale,
|
||||
Limited: gift.Limited, SoldOut: gift.SoldOut, Birthday: gift.Birthday,
|
||||
RequirePremium: gift.RequirePremium, LimitedPerUser: gift.LimitedPerUser,
|
||||
PeerColorAvailable: gift.PeerColorAvailable, Auction: gift.Auction,
|
||||
FirstSaleDate: gift.FirstSaleDate, LastSaleDate: gift.LastSaleDate,
|
||||
ResellMinStars: gift.ResellMinStars, PerUserTotal: gift.PerUserTotal,
|
||||
PerUserRemains: gift.PerUserRemains, LockedUntilDate: gift.LockedUntilDate,
|
||||
AuctionSlug: gift.AuctionSlug, GiftsPerRound: gift.GiftsPerRound,
|
||||
AuctionStartDate: gift.AuctionStartDate, UpgradeVariants: gift.UpgradeVariants,
|
||||
Background: cloneBackground(gift.Background), DocumentID: doc.ID64,
|
||||
AnimationValidated: doc.AnimationValidated,
|
||||
}
|
||||
if set, ok := snap.sets[id]; ok {
|
||||
summary.ModelCount, summary.PatternCount, summary.BackdropCount = len(set.Models), len(set.Patterns), len(set.Backdrops)
|
||||
for _, model := range set.Models {
|
||||
if model.Crafted {
|
||||
summary.CraftedModelCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, summary)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Catalog) Bundle(ctx context.Context, giftID int64, includeCollectible bool) (Bundle, error) {
|
||||
snap, err := c.load()
|
||||
if err != nil {
|
||||
return Bundle{}, err
|
||||
}
|
||||
gift, ok := snap.gifts[giftID]
|
||||
if !ok {
|
||||
return Bundle{}, ErrNotFound
|
||||
}
|
||||
base, err := c.readDocument(ctx, snap.documents[gift.DocumentIDs[0]])
|
||||
if err != nil {
|
||||
return Bundle{}, fmt.Errorf("base document %d: %w", gift.DocumentIDs[0], err)
|
||||
}
|
||||
out := Bundle{
|
||||
ManifestSHA256: append([]byte(nil), snap.manifestSHA...),
|
||||
SourceJSON: append([]byte(nil), gift.SourceJSON...),
|
||||
Gift: Gift{ID: gift.ID, Title: gift.Title, Stars: gift.Stars, ConvertStars: gift.ConvertStars,
|
||||
UpgradeStars: gift.UpgradeStars, AvailabilityTotal: gift.AvailabilityTotal,
|
||||
AvailabilityRemains: gift.AvailabilityRemains, Limited: gift.Limited, SoldOut: gift.SoldOut,
|
||||
Birthday: gift.Birthday, RequirePremium: gift.RequirePremium, LimitedPerUser: gift.LimitedPerUser,
|
||||
PeerColorAvailable: gift.PeerColorAvailable, Auction: gift.Auction,
|
||||
AvailabilityResale: gift.AvailabilityResale, FirstSaleDate: gift.FirstSaleDate,
|
||||
LastSaleDate: gift.LastSaleDate, ResellMinStars: gift.ResellMinStars,
|
||||
PerUserTotal: gift.PerUserTotal, PerUserRemains: gift.PerUserRemains,
|
||||
LockedUntilDate: gift.LockedUntilDate, AuctionSlug: gift.AuctionSlug,
|
||||
GiftsPerRound: gift.GiftsPerRound, AuctionStartDate: gift.AuctionStartDate,
|
||||
UpgradeVariants: gift.UpgradeVariants, Background: cloneBackground(gift.Background),
|
||||
DocumentID: gift.DocumentIDs[0]},
|
||||
BaseDocument: base,
|
||||
}
|
||||
if !includeCollectible {
|
||||
return out, nil
|
||||
}
|
||||
set, ok := snap.sets[giftID]
|
||||
if !ok {
|
||||
return Bundle{}, fmt.Errorf("%w: gift %d has no collectible attribute set", ErrInvalid, giftID)
|
||||
}
|
||||
collectible := &CollectibleSet{
|
||||
Models: make([]Model, 0, len(set.Models)), Patterns: make([]Pattern, 0, len(set.Patterns)),
|
||||
Backdrops: make([]Backdrop, 0, len(set.Backdrops)),
|
||||
}
|
||||
for _, value := range set.Models {
|
||||
doc, err := c.readDocument(ctx, snap.documents[value.DocumentID])
|
||||
if err != nil {
|
||||
return Bundle{}, fmt.Errorf("model %q document %d: %w", value.Name, value.DocumentID, err)
|
||||
}
|
||||
collectible.Models = append(collectible.Models, Model{Name: value.Name, DocumentID: value.DocumentID, Crafted: value.Crafted, Rarity: value.Rarity, Document: doc})
|
||||
}
|
||||
for _, value := range set.Patterns {
|
||||
doc, err := c.readDocument(ctx, snap.documents[value.DocumentID])
|
||||
if err != nil {
|
||||
return Bundle{}, fmt.Errorf("pattern %q document %d: %w", value.Name, value.DocumentID, err)
|
||||
}
|
||||
collectible.Patterns = append(collectible.Patterns, Pattern{Name: value.Name, DocumentID: value.DocumentID, Rarity: value.Rarity, Document: doc})
|
||||
}
|
||||
for _, value := range set.Backdrops {
|
||||
collectible.Backdrops = append(collectible.Backdrops, Backdrop{Name: value.Name, BackdropID: value.BackdropID,
|
||||
CenterColor: value.CenterColor, EdgeColor: value.EdgeColor, PatternColor: value.PatternColor,
|
||||
TextColor: value.TextColor, Rarity: value.Rarity})
|
||||
}
|
||||
out.Collectible = collectible
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func cloneBackground(value *Background) *Background {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
copy := *value
|
||||
return ©
|
||||
}
|
||||
|
||||
func (c *Catalog) load() (*snapshot, error) {
|
||||
c.once.Do(func() {
|
||||
if c.root == "" {
|
||||
c.err = ErrUnavailable
|
||||
return
|
||||
}
|
||||
manifestPath := filepath.Join(c.root, "manifest.json")
|
||||
raw, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
c.err = fmt.Errorf("%w: %v", ErrUnavailable, err)
|
||||
return
|
||||
}
|
||||
var value manifest
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
c.err = fmt.Errorf("%w: decode manifest: %v", ErrInvalid, err)
|
||||
return
|
||||
}
|
||||
if value.Schema != manifestSchema || value.GiftCount != len(value.Gifts) || len(value.Gifts) == 0 {
|
||||
c.err = fmt.Errorf("%w: unexpected schema or gift count", ErrInvalid)
|
||||
return
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
snap := &snapshot{manifestSHA: append([]byte(nil), sum[:]...), gifts: make(map[int64]giftManifest, len(value.Gifts)),
|
||||
sets: make(map[int64]collectibleManifest, len(value.UpgradeAttributeSets)), documents: make(map[int64]documentManifest, len(value.Documents))}
|
||||
for _, doc := range value.Documents {
|
||||
if doc.ID64 <= 0 || doc.File.Size <= 0 || len(doc.File.SHA256) != 64 || strings.TrimSpace(doc.File.Path) == "" {
|
||||
c.err = fmt.Errorf("%w: invalid document %d", ErrInvalid, doc.ID64)
|
||||
return
|
||||
}
|
||||
if _, duplicate := snap.documents[doc.ID64]; duplicate {
|
||||
c.err = fmt.Errorf("%w: duplicate document %d", ErrInvalid, doc.ID64)
|
||||
return
|
||||
}
|
||||
snap.documents[doc.ID64] = doc
|
||||
}
|
||||
for _, gift := range value.Gifts {
|
||||
if gift.Kind != "regular" || gift.ID <= 0 || gift.Stars <= 0 || len(gift.DocumentIDs) != 1 {
|
||||
c.err = fmt.Errorf("%w: invalid gift %d", ErrInvalid, gift.ID)
|
||||
return
|
||||
}
|
||||
if _, ok := snap.documents[gift.DocumentIDs[0]]; !ok {
|
||||
c.err = fmt.Errorf("%w: gift %d document missing", ErrInvalid, gift.ID)
|
||||
return
|
||||
}
|
||||
if _, duplicate := snap.gifts[gift.ID]; duplicate {
|
||||
c.err = fmt.Errorf("%w: duplicate gift %d", ErrInvalid, gift.ID)
|
||||
return
|
||||
}
|
||||
snap.gifts[gift.ID] = gift
|
||||
snap.ordered = append(snap.ordered, gift.ID)
|
||||
}
|
||||
for _, set := range value.UpgradeAttributeSets {
|
||||
if _, ok := snap.gifts[set.GiftID64]; !ok || set.AttributeCount != len(set.Models)+len(set.Patterns)+len(set.Backdrops) ||
|
||||
len(set.Models) == 0 || len(set.Patterns) == 0 || len(set.Backdrops) == 0 {
|
||||
c.err = fmt.Errorf("%w: invalid collectible set %d", ErrInvalid, set.GiftID64)
|
||||
return
|
||||
}
|
||||
if _, duplicate := snap.sets[set.GiftID64]; duplicate {
|
||||
c.err = fmt.Errorf("%w: duplicate collectible set %d", ErrInvalid, set.GiftID64)
|
||||
return
|
||||
}
|
||||
for _, model := range set.Models {
|
||||
if _, ok := snap.documents[model.DocumentID]; !ok || !validRarity(model.Rarity, model.Crafted) {
|
||||
c.err = fmt.Errorf("%w: invalid model %q", ErrInvalid, model.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, pattern := range set.Patterns {
|
||||
if _, ok := snap.documents[pattern.DocumentID]; !ok || !validRarity(pattern.Rarity, false) {
|
||||
c.err = fmt.Errorf("%w: invalid pattern %q", ErrInvalid, pattern.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, backdrop := range set.Backdrops {
|
||||
if backdrop.BackdropID < 0 || !validRarity(backdrop.Rarity, false) {
|
||||
c.err = fmt.Errorf("%w: invalid backdrop %q", ErrInvalid, backdrop.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
snap.sets[set.GiftID64] = set
|
||||
}
|
||||
sort.SliceStable(snap.ordered, func(i, j int) bool { return snap.gifts[snap.ordered[i]].Index < snap.gifts[snap.ordered[j]].Index })
|
||||
c.snap = snap
|
||||
})
|
||||
return c.snap, c.err
|
||||
}
|
||||
|
||||
func validRarity(rarity Rarity, crafted bool) bool {
|
||||
if rarity.Kind == "permille" {
|
||||
return !crafted && rarity.Permille != nil && *rarity.Permille > 0 && *rarity.Permille <= 1000
|
||||
}
|
||||
return crafted && rarity.Permille == nil && (rarity.Kind == "uncommon" || rarity.Kind == "rare" || rarity.Kind == "epic" || rarity.Kind == "legendary")
|
||||
}
|
||||
|
||||
func (c *Catalog) readDocument(ctx context.Context, doc documentManifest) (Document, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Document{}, err
|
||||
}
|
||||
root, err := filepath.Abs(c.root)
|
||||
if err != nil {
|
||||
return Document{}, err
|
||||
}
|
||||
clean := filepath.Clean(filepath.FromSlash(doc.File.Path))
|
||||
if filepath.IsAbs(clean) {
|
||||
return Document{}, ErrInvalid
|
||||
}
|
||||
full := filepath.Join(root, clean)
|
||||
rel, err := filepath.Rel(root, full)
|
||||
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
return Document{}, ErrInvalid
|
||||
}
|
||||
data, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
return Document{}, err
|
||||
}
|
||||
if int64(len(data)) != doc.File.Size {
|
||||
return Document{}, fmt.Errorf("%w: size mismatch", ErrInvalid)
|
||||
}
|
||||
sum := sha256.Sum256(data)
|
||||
if !strings.EqualFold(hex.EncodeToString(sum[:]), doc.File.SHA256) {
|
||||
return Document{}, fmt.Errorf("%w: sha256 mismatch", ErrInvalid)
|
||||
}
|
||||
name := strings.TrimSpace(doc.FileName)
|
||||
if name == "" {
|
||||
name = filepath.Base(clean)
|
||||
}
|
||||
return Document{ID: doc.ID64, FileName: name, Path: doc.File.Path, Size: doc.File.Size,
|
||||
SHA256: strings.ToLower(doc.File.SHA256), AnimationValidated: doc.AnimationValidated,
|
||||
ValidationError: doc.ValidationError, Data: data}, nil
|
||||
}
|
||||
119
internal/officialgifts/catalog_test.go
Normal file
119
internal/officialgifts/catalog_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package officialgifts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCatalogVerifiesSelectedDocument(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
data := []byte("official-tgs")
|
||||
sum := sha256.Sum256(data)
|
||||
if err := os.MkdirAll(filepath.Join(root, "documents"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "documents", "10.tgs"), data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value := manifest{Schema: manifestSchema, GiftCount: 1,
|
||||
Gifts: []giftManifest{{Index: 0, Kind: "regular", ID: 1, Stars: 10, ConvertStars: 5, DocumentIDs: []int64{10}}},
|
||||
Documents: []documentManifest{{ID64: 10, FileName: "gift.tgs", File: fileArtifact{Path: "documents/10.tgs", Size: int64(len(data)), SHA256: hex.EncodeToString(sum[:])}}},
|
||||
}
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "manifest.json"), raw, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := New(root)
|
||||
items, err := catalog.List(context.Background())
|
||||
if err != nil || len(items) != 1 || items[0].ID != 1 {
|
||||
t.Fatalf("items=%+v err=%v", items, err)
|
||||
}
|
||||
bundle, err := catalog.Bundle(context.Background(), 1, false)
|
||||
if err != nil || string(bundle.BaseDocument.Data) != string(data) {
|
||||
t.Fatalf("bundle=%+v err=%v", bundle, err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "documents", "10.tgs"), []byte("tampered---"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := catalog.Bundle(context.Background(), 1, false); err == nil {
|
||||
t.Fatal("tampered document was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredOfficialSnapshotIsComplete(t *testing.T) {
|
||||
root := os.Getenv("TELESRV_TEST_OFFICIAL_GIFTS_DIR")
|
||||
if root == "" {
|
||||
t.Skip("TELESRV_TEST_OFFICIAL_GIFTS_DIR is not set")
|
||||
}
|
||||
catalog := New(root)
|
||||
items, err := catalog.List(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sets, attributes, crafted, upgradable, craftable int
|
||||
verifiedDocuments := map[int64]struct{}{}
|
||||
for _, item := range items {
|
||||
if item.CanUpgrade() {
|
||||
upgradable++
|
||||
}
|
||||
if item.CanCraft() {
|
||||
craftable++
|
||||
}
|
||||
include := item.ModelCount+item.PatternCount+item.BackdropCount > 0
|
||||
bundle, err := catalog.Bundle(context.Background(), item.ID, include)
|
||||
if err != nil {
|
||||
t.Fatalf("gift %d: %v", item.ID, err)
|
||||
}
|
||||
verifiedDocuments[bundle.BaseDocument.ID] = struct{}{}
|
||||
if bundle.Collectible == nil {
|
||||
continue
|
||||
}
|
||||
sets++
|
||||
attributes += len(bundle.Collectible.Models) + len(bundle.Collectible.Patterns) + len(bundle.Collectible.Backdrops)
|
||||
for _, model := range bundle.Collectible.Models {
|
||||
verifiedDocuments[model.Document.ID] = struct{}{}
|
||||
if model.Crafted {
|
||||
crafted++
|
||||
}
|
||||
}
|
||||
for _, pattern := range bundle.Collectible.Patterns {
|
||||
verifiedDocuments[pattern.Document.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(items) != 149 || sets != 116 || attributes != 40332 || crafted != 108 || upgradable != 114 || craftable != 2 || len(verifiedDocuments) != 8333 {
|
||||
t.Fatalf("gifts=%d sets=%d attributes=%d crafted=%d upgradable=%d craftable=%d documents=%d",
|
||||
len(items), sets, attributes, crafted, upgradable, craftable, len(verifiedDocuments))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGiftSummaryCapabilitiesRequireCompleteOfficialFacts(t *testing.T) {
|
||||
complete := GiftSummary{UpgradeStars: 25, ModelCount: 1, PatternCount: 1, BackdropCount: 1}
|
||||
if !complete.CanUpgrade() || complete.CanCraft() {
|
||||
t.Fatalf("complete regular pool capabilities = upgrade:%v craft:%v", complete.CanUpgrade(), complete.CanCraft())
|
||||
}
|
||||
craftable := complete
|
||||
craftable.CraftedModelCount = 1
|
||||
if !craftable.CanUpgrade() || !craftable.CanCraft() {
|
||||
t.Fatalf("crafted pool capabilities = upgrade:%v craft:%v", craftable.CanUpgrade(), craftable.CanCraft())
|
||||
}
|
||||
for name, invalid := range map[string]GiftSummary{
|
||||
"zero upgrade price": {ModelCount: 1, PatternCount: 1, BackdropCount: 1, CraftedModelCount: 1},
|
||||
"missing model": {UpgradeStars: 25, PatternCount: 1, BackdropCount: 1, CraftedModelCount: 1},
|
||||
"missing pattern": {UpgradeStars: 25, ModelCount: 1, BackdropCount: 1, CraftedModelCount: 1},
|
||||
"missing backdrop": {UpgradeStars: 25, ModelCount: 1, PatternCount: 1, CraftedModelCount: 1},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if invalid.CanUpgrade() || invalid.CanCraft() {
|
||||
t.Fatalf("invalid facts advertised capabilities: %+v", invalid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue