fix: sync collectible upgrade preview pools

This commit is contained in:
A 2026-07-22 13:06:49 +08:00
parent eba402946a
commit de233a4c18
16 changed files with 552 additions and 136 deletions

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title>
<script type="module" crossorigin src="/assets/index-D9dH2J7N.js"></script>
<script type="module" crossorigin src="/assets/index-Duge82ST.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DHdrFM5j.css">
</head>
<body>

View file

@ -316,7 +316,7 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.noPool": "No collectible pool published",
"collectibles.noPoolHint": "Publish models, patterns and backdrops to enable upgrades.",
"collectibles.publishNew": "Publish a new immutable revision",
"collectibles.immutableHint": "Dry-run checks every file and rarity total before the revision becomes active.",
"collectibles.immutableHint": "Dry-run checks every file and the attribute-pool structure before the revision becomes active.",
"collectibles.upgradeStars": "Upgrade price in Stars",
"collectibles.supply": "Unique supply",
"collectibles.slug": "Public slug prefix",
@ -327,7 +327,9 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.pattern": "Pattern",
"collectibles.backdrop": "Backdrop",
"collectibles.rarity": "Rarity ‰",
"collectibles.rarityHint": "Permille values are relative regular-upgrade weights; their total does not need to equal 1000.",
"collectibles.rarityHint": "Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.",
"collectibles.minimumAttributes": "Models, patterns, and backdrops must each contain at least two attributes.",
"collectibles.duplicateBackdropID": "Backdrop IDs must be unique within the pool.",
"collectibles.colorHint": "Colors are stored as 24-bit RGB values.",
"collectibles.addAttribute": "Add",
"collectibles.remove": "Remove attribute",
@ -686,7 +688,7 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.noPool": "尚未发布 Collectibles 属性池",
"collectibles.noPoolHint": "发布模型、图案与背景后,客户端即可升级为唯一礼物。",
"collectibles.publishNew": "发布新的不可变版本",
"collectibles.immutableHint": "Dry-run 会校验全部文件和稀有度总和,通过后才切换为当前版本。",
"collectibles.immutableHint": "Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。",
"collectibles.upgradeStars": "升级价格 Stars",
"collectibles.supply": "唯一礼物总量",
"collectibles.slug": "公开 Slug 前缀",
@ -697,7 +699,9 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.pattern": "图案",
"collectibles.backdrop": "背景",
"collectibles.rarity": "稀有度 ‰",
"collectibles.rarityHint": "Permille 是普通升级的相对权重,不要求每类合计正好为 1000。",
"collectibles.rarityHint": "每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。",
"collectibles.minimumAttributes": "模型、图案和背景每类都必须至少包含两个属性。",
"collectibles.duplicateBackdropID": "同一属性池中的背景 ID 必须唯一。",
"collectibles.colorHint": "颜色会按 24 位 RGB 数值保存。",
"collectibles.addAttribute": "添加",
"collectibles.remove": "删除属性",
@ -1056,7 +1060,7 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.noPool": "Нет опубликованного пула коллекционных предметов",
"collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.",
"collectibles.publishNew": "Опубликовать новую неизменяемую версию",
"collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и итоговые показатели редкости перед тем, как версия станет активной.",
"collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.",
"collectibles.upgradeStars": "Цена улучшения в Звездах",
"collectibles.supply": "Уникальный тираж",
"collectibles.slug": "Публичный префикс ссылки (slug)",
@ -1067,7 +1071,9 @@ const translations: Record<Language, Record<string, string>> = {
"collectibles.pattern": "Узор",
"collectibles.backdrop": "Фон",
"collectibles.rarity": "Редкость ‰",
"collectibles.rarityHint": "Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.",
"collectibles.rarityHint": "В каждой категории должно быть не менее двух атрибутов. При добавлении и удалении веса permille перераспределяются до 1000.",
"collectibles.minimumAttributes": "Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.",
"collectibles.duplicateBackdropID": "ID фонов в одном наборе должны быть уникальными.",
"collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.",
"collectibles.addAttribute": "Добавить",
"collectibles.remove": "Удалить атрибут",

View file

@ -31,8 +31,38 @@ type BackdropDraft = {
let draftSequence = 0;
const nextKey = (kind: string) => `${kind}-${++draftSequence}`;
const newAnimated = (kind: string): AnimatedDraft => ({ key: nextKey(kind), name: "", rarity: "1000", sortOrder: "0", file: null, animation: null, fileError: "" });
const newBackdrop = (): BackdropDraft => ({ key: nextKey("backdrop"), name: "", backdropID: "1", rarity: "1000", sortOrder: "0", center: "#6f5bea", edge: "#34278f", pattern: "#a89df5", text: "#ffffff" });
const backdropPalettes = [
{ center: "#6f5bea", edge: "#34278f", pattern: "#a89df5", text: "#ffffff" },
{ center: "#32a86b", edge: "#17613e", pattern: "#8ee0b3", text: "#ffffff" },
{ center: "#df8d2f", edge: "#8c421e", pattern: "#ffd08a", text: "#ffffff" },
{ center: "#d95878", edge: "#7b2944", pattern: "#f5a1b6", text: "#ffffff" }
];
function rebalanceRarity<T extends { rarity: string }>(rows: T[]): T[] {
if (!rows.length) return rows;
const base = Math.floor(1000 / rows.length);
const remainder = 1000 % rows.length;
return rows.map((row, index) => ({ ...row, rarity: String(base + (index < remainder ? 1 : 0)) }));
}
const newAnimated = (kind: string, sortOrder: number): AnimatedDraft => ({
key: nextKey(kind), name: "", rarity: "1", sortOrder: String(sortOrder), file: null, animation: null, fileError: ""
});
function newBackdrop(rows: BackdropDraft[]): BackdropDraft {
const backdropID = rows.reduce((maximum, row) => {
const value = Number(row.backdropID);
return Number.isInteger(value) ? Math.max(maximum, value) : maximum;
}, 0) + 1;
const colors = backdropPalettes[rows.length % backdropPalettes.length];
return { key: nextKey("backdrop"), name: "", backdropID: String(backdropID), rarity: "1", sortOrder: String(rows.length), ...colors };
}
const initialAnimated = (kind: string) => rebalanceRarity([newAnimated(kind, 0), newAnimated(kind, 1)]);
const initialBackdrops = () => {
const first = newBackdrop([]);
return rebalanceRarity([first, newBackdrop([first])]);
};
function AnimationPreview({ data, compact = false }: { data: AnimationData; compact?: boolean }) {
const host = useRef<HTMLDivElement>(null);
@ -87,9 +117,9 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
const [supplyTotal, setSupplyTotal] = useState("1000");
const [slugPrefix, setSlugPrefix] = useState(`gift-${gift.GiftID}`);
const [reason, setReason] = useState("");
const [models, setModels] = useState<AnimatedDraft[]>([newAnimated("model")]);
const [patterns, setPatterns] = useState<AnimatedDraft[]>([newAnimated("pattern")]);
const [backdrops, setBackdrops] = useState<BackdropDraft[]>([newBackdrop()]);
const [models, setModels] = useState<AnimatedDraft[]>(() => initialAnimated("model"));
const [patterns, setPatterns] = useState<AnimatedDraft[]>(() => initialAnimated("pattern"));
const [backdrops, setBackdrops] = useState<BackdropDraft[]>(initialBackdrops);
useEffect(() => {
let cancelled = false;
@ -131,6 +161,9 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
function buildForm(confirm: boolean, commandID = "") {
if (!reason.trim()) throw new Error(t("action.reasonRequired"));
if (models.length < 2 || patterns.length < 2 || backdrops.length < 2) throw new Error(t("collectibles.minimumAttributes"));
const backdropIDs = backdrops.map((row) => Number(row.backdropID));
if (new Set(backdropIDs).size !== backdropIDs.length) throw new Error(t("collectibles.duplicateBackdropID"));
for (const row of [...models, ...patterns]) if (!row.file) throw new Error(t("collectibles.fileRequired"));
const form = new FormData();
const animatedMetadata = (rows: AnimatedDraft[]) => rows.map((row) => ({ name: row.name.trim(), rarity_permille: Number(row.rarity), sort_order: Number(row.sortOrder), file_key: row.key }));
@ -168,7 +201,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
<section className="collectible-section">
<div className="collectible-section-head">
<div><strong>{t(`collectibles.${kind}`)}</strong><span>{t("collectibles.rarityHint")}</span></div>
<div className="collectible-section-tools"><Badge tone={rarityTotals[kind] > 0 ? "good" : "neutral"}>{rarityTotals[kind]}</Badge><button className="btn compact-btn" type="button" onClick={() => { setRows([...rows, newAnimated(kind === "models" ? "model" : "pattern")]); invalidate(); }}><Plus size={13} />{t("collectibles.addAttribute")}</button></div>
<div className="collectible-section-tools"><Badge tone={rarityTotals[kind] > 0 ? "good" : "neutral"}>{rarityTotals[kind]}</Badge><button className="btn compact-btn" type="button" onClick={() => { setRows(rebalanceRarity([...rows, newAnimated(kind === "models" ? "model" : "pattern", rows.length)])); invalidate(); }}><Plus size={13} />{t("collectibles.addAttribute")}</button></div>
</div>
<div className="collectible-rows">
{rows.map((row, index) => <div className="collectible-row animated" key={row.key}>
@ -178,7 +211,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
<label><span>{t("gifts.sortOrder")}</span><input type="number" value={row.sortOrder} onChange={(e) => updateAnimated(kind, row.key, { sortOrder: e.target.value })} /></label>
<label className="collectible-file"><span>{t("gifts.animation")}</span><input type="file" accept=".tgs,.json,.lottie,application/json,application/x-tgsticker" onChange={(e) => void chooseFile(kind, row, e.target.files?.[0] ?? null)} /><em><FileJson2 size={13} />{row.file?.name ?? t("gifts.chooseFile")}</em></label>
<div className="collectible-inline-preview">{row.animation ? <AnimationPreview data={row.animation} compact /> : <Sparkles size={16} />}</div>
<button className="icon-btn danger" type="button" disabled={rows.length === 1} onClick={() => { setRows(rows.filter((value) => value.key !== row.key)); invalidate(); }} aria-label={t("collectibles.remove")}><Trash2 size={14} /></button>
<button className="icon-btn danger" type="button" disabled={rows.length <= 2} onClick={() => { setRows(rebalanceRarity(rows.filter((value) => value.key !== row.key))); invalidate(); }} aria-label={t("collectibles.remove")}><Trash2 size={14} /></button>
{row.fileError && <span className="collectible-file-error">{row.fileError}</span>}
</div>)}
</div>
@ -211,7 +244,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
{renderAnimatedRows("models", models, setModels)}
{renderAnimatedRows("patterns", patterns, setPatterns)}
<section className="collectible-section">
<div className="collectible-section-head"><div><strong>{t("collectibles.backdrops")}</strong><span>{t("collectibles.colorHint")}</span></div><div className="collectible-section-tools"><Badge tone={rarityTotals.backdrops > 0 ? "good" : "neutral"}>{rarityTotals.backdrops}</Badge><button className="btn compact-btn" type="button" onClick={() => { setBackdrops([...backdrops, newBackdrop()]); invalidate(); }}><Plus size={13} />{t("collectibles.addAttribute")}</button></div></div>
<div className="collectible-section-head"><div><strong>{t("collectibles.backdrops")}</strong><span>{t("collectibles.colorHint")}</span></div><div className="collectible-section-tools"><Badge tone={rarityTotals.backdrops > 0 ? "good" : "neutral"}>{rarityTotals.backdrops}</Badge><button className="btn compact-btn" type="button" onClick={() => { setBackdrops(rebalanceRarity([...backdrops, newBackdrop(backdrops)])); invalidate(); }}><Plus size={13} />{t("collectibles.addAttribute")}</button></div></div>
<div className="collectible-rows">{backdrops.map((row, index) => <div className="collectible-row backdrop" key={row.key}>
<div className="collectible-row-index">{index + 1}</div>
<label><span>{t("common.name")}</span><input value={row.name} maxLength={128} onChange={(e) => { setBackdrops(backdrops.map((value) => value.key === row.key ? { ...value, name: e.target.value } : value)); invalidate(); }} /></label>
@ -220,7 +253,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
<label><span>{t("gifts.sortOrder")}</span><input type="number" value={row.sortOrder} onChange={(e) => { setBackdrops(backdrops.map((value) => value.key === row.key ? { ...value, sortOrder: e.target.value } : value)); invalidate(); }} /></label>
{(["center", "edge", "pattern", "text"] as const).map((field) => <label className="collectible-color" key={field}><span>{t(`collectibles.color.${field}`)}</span><input type="color" value={row[field]} onChange={(e) => { setBackdrops(backdrops.map((value) => value.key === row.key ? { ...value, [field]: e.target.value } : value)); invalidate(); }} /></label>)}
<div className="collectible-backdrop-preview" style={{ background: `radial-gradient(circle, ${row.center}, ${row.edge})`, color: row.text }}>Aa</div>
<button className="icon-btn danger" type="button" disabled={backdrops.length === 1} onClick={() => { setBackdrops(backdrops.filter((value) => value.key !== row.key)); invalidate(); }} aria-label={t("collectibles.remove")}><Trash2 size={14} /></button>
<button className="icon-btn danger" type="button" disabled={backdrops.length <= 2} onClick={() => { setBackdrops(rebalanceRarity(backdrops.filter((value) => value.key !== row.key))); invalidate(); }} aria-label={t("collectibles.remove")}><Trash2 size={14} /></button>
</div>)}</div>
</section>
</section>

View file

@ -0,0 +1,10 @@
DROP TRIGGER IF EXISTS star_gift_catalog_collectible_preview_activation ON public.star_gift_catalog;
DROP FUNCTION IF EXISTS public.telesrv_validate_collectible_preview_activation();
UPDATE public.star_gift_catalog c
SET collectible_revision_id = repair.collectible_revision_id, updated_at = now()
FROM public.star_gift_collectible_preview_repairs repair
WHERE c.gift_id = repair.gift_id
AND c.collectible_revision_id IS NULL;
DROP TABLE public.star_gift_collectible_preview_repairs;

View file

@ -0,0 +1,88 @@
-- TDesktop deduplicates upgrade-preview models/patterns by document identity and can only
-- finish each attribute spinner after it has a non-target item. Detach previously published
-- pools that cannot satisfy that client contract; the immutable revisions remain available
-- for audit and for already-issued unique gifts.
CREATE TABLE public.star_gift_collectible_preview_repairs (
gift_id bigint PRIMARY KEY REFERENCES public.star_gift_catalog(gift_id) ON DELETE CASCADE,
collectible_revision_id bigint UNIQUE NOT NULL
REFERENCES public.star_gift_collectible_revisions(id) ON DELETE RESTRICT,
reason text DEFAULT 'insufficient distinct upgrade preview attributes' NOT NULL,
repaired_at timestamp with time zone DEFAULT now() NOT NULL
);
INSERT INTO public.star_gift_collectible_preview_repairs (gift_id, collectible_revision_id)
SELECT c.gift_id, c.collectible_revision_id
FROM public.star_gift_catalog c
JOIN public.star_gift_collectible_revisions r ON r.id = c.collectible_revision_id
WHERE c.collectible_revision_id IS NOT NULL
AND (
r.status <> 'published' OR r.gift_id <> c.gift_id OR
(SELECT count(DISTINCT m.document_id)
FROM public.star_gift_collectible_models m
WHERE m.collectible_revision_id = r.id
AND m.rarity_kind = 'permille' AND NOT m.crafted) < 2 OR
(SELECT count(DISTINCT p.document_id)
FROM public.star_gift_collectible_patterns p
WHERE p.collectible_revision_id = r.id
AND p.rarity_kind = 'permille') < 2 OR
(SELECT count(DISTINCT b.backdrop_id)
FROM public.star_gift_collectible_backdrops b
WHERE b.collectible_revision_id = r.id
AND b.rarity_kind = 'permille') < 2
);
UPDATE public.star_gift_catalog c
SET collectible_revision_id = NULL, updated_at = now()
FROM public.star_gift_collectible_preview_repairs repair
WHERE c.gift_id = repair.gift_id
AND c.collectible_revision_id = repair.collectible_revision_id;
-- Keep the same invariant at the final activation boundary. Application validation gives the
-- operator a precise error first; this trigger also protects imports or maintenance SQL that
-- attempts to expose a malformed published revision directly.
CREATE FUNCTION public.telesrv_validate_collectible_preview_activation() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
revision_gift_id bigint;
revision_status text;
BEGIN
IF NEW.collectible_revision_id IS NULL THEN
RETURN NEW;
END IF;
SELECT gift_id, status INTO revision_gift_id, revision_status
FROM public.star_gift_collectible_revisions
WHERE id = NEW.collectible_revision_id;
IF NOT FOUND OR revision_gift_id <> NEW.gift_id OR revision_status <> 'published' THEN
RAISE EXCEPTION 'collectible preview revision must be published for the same gift'
USING ERRCODE = '23514';
END IF;
IF (SELECT count(DISTINCT document_id)
FROM public.star_gift_collectible_models
WHERE collectible_revision_id = NEW.collectible_revision_id
AND rarity_kind = 'permille' AND NOT crafted) < 2 THEN
RAISE EXCEPTION 'collectible model preview requires two distinct documents'
USING ERRCODE = '23514';
END IF;
IF (SELECT count(DISTINCT document_id)
FROM public.star_gift_collectible_patterns
WHERE collectible_revision_id = NEW.collectible_revision_id
AND rarity_kind = 'permille') < 2 THEN
RAISE EXCEPTION 'collectible pattern preview requires two distinct documents'
USING ERRCODE = '23514';
END IF;
IF (SELECT count(DISTINCT backdrop_id)
FROM public.star_gift_collectible_backdrops
WHERE collectible_revision_id = NEW.collectible_revision_id
AND rarity_kind = 'permille') < 2 THEN
RAISE EXCEPTION 'collectible backdrop preview requires two distinct IDs'
USING ERRCODE = '23514';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER star_gift_catalog_collectible_preview_activation
BEFORE INSERT OR UPDATE OF collectible_revision_id ON public.star_gift_catalog
FOR EACH ROW EXECUTE FUNCTION public.telesrv_validate_collectible_preview_activation();

View file

@ -798,9 +798,18 @@ func TestPublishStarGiftCollectiblesDryRunThenConfirm(t *testing.T) {
svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: gifts, Now: fixedNow})
base := PublishStarGiftCollectiblesRequest{
GiftID: 11, UpgradeStars: 125, SupplyTotal: 100, SlugPrefix: "cake",
Models: []StarGiftCollectibleAnimationUpload{{Name: "Ruby", RarityPermille: 1000, FileKey: "model-0", FileName: "ruby.lottie", Data: []byte("model")}},
Patterns: []StarGiftCollectibleAnimationUpload{{Name: "Stars", RarityPermille: 1000, FileKey: "pattern-0", FileName: "stars.tgs", Data: []byte("pattern")}},
Backdrops: []StarGiftCollectibleBackdropInput{{Name: "Night", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityPermille: 1000}},
Models: []StarGiftCollectibleAnimationUpload{
{Name: "Ruby", RarityPermille: 500, FileKey: "model-0", FileName: "ruby.lottie", Data: []byte("model")},
{Name: "Sapphire", RarityPermille: 500, FileKey: "model-1", FileName: "sapphire.lottie", Data: []byte("model-1")},
},
Patterns: []StarGiftCollectibleAnimationUpload{
{Name: "Stars", RarityPermille: 500, FileKey: "pattern-0", FileName: "stars.tgs", Data: []byte("pattern")},
{Name: "Moons", RarityPermille: 500, FileKey: "pattern-1", FileName: "moons.tgs", Data: []byte("pattern-1")},
},
Backdrops: []StarGiftCollectibleBackdropInput{
{Name: "Night", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityPermille: 500},
{Name: "Day", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityPermille: 500},
},
}
base.CommandMeta = CommandMeta{CommandID: "dry-collectibles", Actor: "ops", Reason: "pool", DryRun: true}
preview, err := svc.PublishStarGiftCollectibles(context.Background(), base)
@ -814,6 +823,45 @@ func TestPublishStarGiftCollectiblesDryRunThenConfirm(t *testing.T) {
}
}
func TestPublishStarGiftCollectiblesRejectsUnsafeClientPreviewPool(t *testing.T) {
valid := func() PublishStarGiftCollectiblesRequest {
return PublishStarGiftCollectiblesRequest{
CommandMeta: CommandMeta{CommandID: "unsafe-pool", Actor: "ops", Reason: "regression", DryRun: true},
GiftID: 11, UpgradeStars: 125, SupplyTotal: 100, SlugPrefix: "cake",
Models: []StarGiftCollectibleAnimationUpload{
{Name: "Ruby", RarityPermille: 500, FileName: "ruby.lottie", Data: []byte("ruby")},
{Name: "Sapphire", RarityPermille: 500, FileName: "sapphire.lottie", Data: []byte("sapphire")},
},
Patterns: []StarGiftCollectibleAnimationUpload{
{Name: "Stars", RarityPermille: 500, FileName: "stars.lottie", Data: []byte("stars")},
{Name: "Moons", RarityPermille: 500, FileName: "moons.lottie", Data: []byte("moons")},
},
Backdrops: []StarGiftCollectibleBackdropInput{
{Name: "Night", BackdropID: 1, RarityPermille: 500},
{Name: "Day", BackdropID: 2, RarityPermille: 500},
},
}
}
tests := map[string]func(*PublishStarGiftCollectiblesRequest){
"single model": func(req *PublishStarGiftCollectiblesRequest) { req.Models = req.Models[:1] },
"single pattern": func(req *PublishStarGiftCollectiblesRequest) { req.Patterns = req.Patterns[:1] },
"single backdrop": func(req *PublishStarGiftCollectiblesRequest) { req.Backdrops = req.Backdrops[:1] },
"duplicate backdrop id": func(req *PublishStarGiftCollectiblesRequest) {
req.Backdrops[1].BackdropID = req.Backdrops[0].BackdropID
},
}
for name, mutate := range tests {
t.Run(name, func(t *testing.T) {
req := valid()
mutate(&req)
svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: &fakeGiftsService{}, Now: fixedNow})
if _, err := svc.PublishStarGiftCollectibles(context.Background(), req); !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) {
t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err)
}
})
}
}
func TestImportOfficialStarGiftPreservesCraftedRarityAndPublishesBundle(t *testing.T) {
permille := 922
source := &fakeOfficialGiftsSource{bundle: officialgifts.Bundle{
@ -828,10 +876,17 @@ func TestImportOfficialStarGiftPreservesCraftedRarityAndPublishesBundle(t *testi
Collectible: &officialgifts.CollectibleSet{
Models: []officialgifts.Model{
{Name: "Regular", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 2, FileName: "regular.tgs", SHA256: strings.Repeat("b", 64), Data: []byte("regular")}},
{Name: "Regular Two", DocumentID: 5, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 5, FileName: "regular-two.tgs", SHA256: strings.Repeat("e", 64), Data: []byte("regular-two")}},
{Name: "Crafted", DocumentID: 3, Crafted: true, Rarity: officialgifts.Rarity{Kind: "legendary"}, Document: officialgifts.Document{ID: 3, FileName: "crafted.tgs", SHA256: strings.Repeat("c", 64), Data: []byte("crafted")}},
},
Patterns: []officialgifts.Pattern{{Name: "Pattern", DocumentID: 4, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 4, FileName: "pattern.tgs", SHA256: strings.Repeat("d", 64), Data: []byte("pattern")}}},
Backdrops: []officialgifts.Backdrop{{Name: "Black", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}}},
Patterns: []officialgifts.Pattern{
{Name: "Pattern", DocumentID: 4, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 4, FileName: "pattern.tgs", SHA256: strings.Repeat("d", 64), Data: []byte("pattern")}},
{Name: "Pattern Two", DocumentID: 6, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 6, FileName: "pattern-two.tgs", SHA256: strings.Repeat("f", 64), Data: []byte("pattern-two")}},
},
Backdrops: []officialgifts.Backdrop{
{Name: "Black", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}},
{Name: "White", BackdropID: 1, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}},
},
},
}}
gifts := &fakeGiftsService{}
@ -848,7 +903,7 @@ func TestImportOfficialStarGiftPreservesCraftedRarityAndPublishesBundle(t *testi
t.Fatalf("result=%+v err=%v create=%d", result, err, gifts.createCalls)
}
models := gifts.lastBundle.Collectible.Models
if len(models) != 2 || !models[1].Crafted || models[1].RarityKind != domain.StarGiftRarityLegendary || models[1].RarityPermille != 0 ||
if len(models) != 3 || !models[2].Crafted || models[2].RarityKind != domain.StarGiftRarityLegendary || models[2].RarityPermille != 0 ||
models[0].RarityPermille != 922 || gifts.lastBundle.Collectible.Backdrops[0].BackdropID != 0 {
t.Fatalf("imported models=%+v backdrops=%+v", models, gifts.lastBundle.Collectible.Backdrops)
}
@ -880,17 +935,18 @@ func TestImportOfficialStarGiftPublishesThroughRealGiftService(t *testing.T) {
},
BaseDocument: document(1, "gift.json"),
Collectible: &officialgifts.CollectibleSet{
Models: []officialgifts.Model{{
Name: "Model", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille},
Document: document(2, "model.json"),
}},
Patterns: []officialgifts.Pattern{{
Name: "Pattern", DocumentID: 3, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille},
Document: document(3, "pattern.json"),
}},
Backdrops: []officialgifts.Backdrop{{
Name: "Backdrop", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille},
}},
Models: []officialgifts.Model{
{Name: "Model", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(2, "model.json")},
{Name: "Model Two", DocumentID: 4, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(4, "model-two.json")},
},
Patterns: []officialgifts.Pattern{
{Name: "Pattern", DocumentID: 3, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(3, "pattern.json")},
{Name: "Pattern Two", DocumentID: 5, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(5, "pattern-two.json")},
},
Backdrops: []officialgifts.Backdrop{
{Name: "Backdrop", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}},
{Name: "Backdrop Two", BackdropID: 1, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}},
},
},
}}
ctx := context.Background()
@ -911,7 +967,7 @@ func TestImportOfficialStarGiftPublishesThroughRealGiftService(t *testing.T) {
t.Fatalf("catalog=%+v err=%v, want one imported gift", catalog, err)
}
preview, ok, err := giftService.CollectiblePreview(ctx, catalog[0].ID)
if err != nil || !ok || len(preview.Models) != 1 || len(preview.Patterns) != 1 {
if err != nil || !ok || len(preview.Models) != 2 || len(preview.Patterns) != 2 {
t.Fatalf("preview=%+v ok=%v err=%v", preview, ok, err)
}
model := preview.Models[0].Document

View file

@ -120,11 +120,14 @@ func TestAdminAPIImportStarGiftMultipart(t *testing.T) {
func TestAdminAPIPublishStarGiftCollectiblesMultipart(t *testing.T) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
metadata := `{"command_id":"pool-1","actor":"ops","reason":"pool","dry_run":true,"upgrade_stars":125,"supply_total":100,"slug_prefix":"cake","models":[{"name":"Ruby","rarity_permille":1000,"sort_order":0,"file_key":"model-0"}],"patterns":[{"name":"Stars","rarity_permille":1000,"sort_order":0,"file_key":"pattern-0"}],"backdrops":[{"name":"Night","backdrop_id":1,"center_color":1122867,"edge_color":2241348,"pattern_color":3359829,"text_color":16777215,"rarity_permille":1000,"sort_order":0}]}`
metadata := `{"command_id":"pool-1","actor":"ops","reason":"pool","dry_run":true,"upgrade_stars":125,"supply_total":100,"slug_prefix":"cake","models":[{"name":"Ruby","rarity_permille":500,"sort_order":0,"file_key":"model-0"},{"name":"Sapphire","rarity_permille":500,"sort_order":1,"file_key":"model-1"}],"patterns":[{"name":"Stars","rarity_permille":500,"sort_order":0,"file_key":"pattern-0"},{"name":"Moons","rarity_permille":500,"sort_order":1,"file_key":"pattern-1"}],"backdrops":[{"name":"Night","backdrop_id":1,"center_color":1122867,"edge_color":2241348,"pattern_color":3359829,"text_color":16777215,"rarity_permille":500,"sort_order":0},{"name":"Day","backdrop_id":2,"center_color":11189196,"edge_color":7833753,"pattern_color":14544639,"text_color":1118481,"rarity_permille":500,"sort_order":1}]}`
if err := writer.WriteField("metadata", metadata); err != nil {
t.Fatal(err)
}
for key, name := range map[string]string{"model-0": "ruby.lottie", "pattern-0": "stars.tgs"} {
for key, name := range map[string]string{
"model-0": "ruby.lottie", "model-1": "sapphire.lottie",
"pattern-0": "stars.tgs", "pattern-1": "moons.tgs",
} {
part, err := writer.CreateFormFile(key, name)
if err != nil {
t.Fatal(err)
@ -146,8 +149,8 @@ func TestAdminAPIPublishStarGiftCollectiblesMultipart(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if svc.req.GiftID != 11 || len(svc.req.Models) != 1 || svc.req.Models[0].FileName != "ruby.lottie" ||
string(svc.req.Patterns[0].Data) != "pattern-0" || len(svc.req.Backdrops) != 1 {
if svc.req.GiftID != 11 || len(svc.req.Models) != 2 || svc.req.Models[0].FileName != "ruby.lottie" ||
string(svc.req.Patterns[0].Data) != "pattern-0" || len(svc.req.Backdrops) != 2 || svc.req.Backdrops[1].BackdropID != 2 {
t.Fatalf("decoded collectible request = %+v", svc.req)
}
}

View file

@ -132,18 +132,18 @@ func TestCreateCatalogBundleMaterializesPublishableCollectibleDocuments(t *testi
},
Collectible: &domain.StarGiftCollectibleWrite{
UpgradeStars: 100, SupplyTotal: 1000, SlugPrefix: "official-10",
Models: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectibleModel, Name: "Model", RarityKind: domain.StarGiftRarityPermille,
RarityPermille: 1000, Animation: &animation,
}},
Patterns: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille,
RarityPermille: 1000, Animation: &animation,
}},
Backdrops: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", RarityKind: domain.StarGiftRarityPermille,
RarityPermille: 1000,
}},
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,
},
@ -151,7 +151,7 @@ func TestCreateCatalogBundleMaterializesPublishableCollectibleDocuments(t *testi
if err != nil {
t.Fatalf("create official collectible bundle: %v", err)
}
if result.Collectible == nil || len(result.Collectible.Models) != 1 || len(result.Collectible.Patterns) != 1 {
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

View file

@ -3,6 +3,7 @@ package domain
import (
"encoding/base64"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
@ -139,6 +140,7 @@ func (k StarGiftAttributeRarityKind) Valid() bool {
// StarGiftCollectibleAttribute 是已发布属性池的一项。RarityKind/RarityPermille
// 是客户端展示事实;普通升级把非 crafted 的 permille 值当相对权重,不要求合计为 1000。
// 每类仍必须提供至少两个客户端可区分的普通升级属性,否则 TDesktop 的升级滚动无法结束。
type StarGiftCollectibleAttribute struct {
ID int64
CollectibleRevisionID int64
@ -935,7 +937,10 @@ func ValidateStarGiftCollectibleDraft(write StarGiftCollectibleWrite) error {
if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, false); err != nil {
return err
}
return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, false)
if err := validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, false); err != nil {
return err
}
return validateStarGiftUpgradePreviewPool(write, false)
}
// ValidateStarGiftCollectibleWrite validates a complete publish command. Published pools are
@ -950,7 +955,62 @@ func ValidateStarGiftCollectibleWrite(write StarGiftCollectibleWrite) error {
if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, true); err != nil {
return err
}
return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, true)
if err := validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, true); err != nil {
return err
}
return validateStarGiftUpgradePreviewPool(write, true)
}
// validateStarGiftUpgradePreviewPool protects the official-client animation contract. The
// preview response includes the target attribute plus the published selectable pool; TDesktop
// deduplicates models and patterns by document identity and needs a non-target item in every
// category before its spinner can transition to the finished state.
func validateStarGiftUpgradePreviewPool(write StarGiftCollectibleWrite, requireStoredAsset bool) error {
validateAnimated := func(kind StarGiftCollectibleAttributeKind, attributes []StarGiftCollectibleAttribute) error {
selectable := 0
documents := make(map[int64]struct{}, len(attributes))
for _, attribute := range attributes {
if attribute.RarityKind != StarGiftRarityPermille || attribute.Crafted {
continue
}
selectable++
if requireStoredAsset {
if attribute.Document == nil {
return fmt.Errorf("%w: %s preview attribute has no document", ErrStarGiftCollectibleInvalid, kind)
}
documents[attribute.Document.ID] = struct{}{}
}
}
if selectable < 2 {
return fmt.Errorf("%w: %s preview requires at least two selectable attributes", ErrStarGiftCollectibleInvalid, kind)
}
if requireStoredAsset && len(documents) < 2 {
return fmt.Errorf("%w: %s preview requires at least two distinct documents", ErrStarGiftCollectibleInvalid, kind)
}
return nil
}
if err := validateAnimated(StarGiftCollectibleModel, write.Models); err != nil {
return err
}
if err := validateAnimated(StarGiftCollectiblePattern, write.Patterns); err != nil {
return err
}
seenBackdropIDs := make(map[int]struct{}, len(write.Backdrops))
selectableBackdrops := 0
for _, attribute := range write.Backdrops {
if attribute.RarityKind != StarGiftRarityPermille || attribute.Crafted {
continue
}
selectableBackdrops++
if _, exists := seenBackdropIDs[attribute.BackdropID]; exists {
return fmt.Errorf("%w: duplicate backdrop_id %d", ErrStarGiftCollectibleInvalid, attribute.BackdropID)
}
seenBackdropIDs[attribute.BackdropID] = struct{}{}
}
if selectableBackdrops < 2 {
return fmt.Errorf("%w: backdrop preview requires at least two selectable attributes", ErrStarGiftCollectibleInvalid)
}
return nil
}
func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind StarGiftCollectibleAttributeKind, requireStoredAsset bool) error {

View file

@ -48,13 +48,16 @@ func validCollectibleDraft() StarGiftCollectibleWrite {
GiftID: 1, UpgradeStars: 25, SupplyTotal: 100, SlugPrefix: "official-1", CommandID: "test",
Models: []StarGiftCollectibleAttribute{
{Kind: StarGiftCollectibleModel, Name: "Regular", RarityKind: StarGiftRarityPermille, RarityPermille: 922, Animation: animation},
{Kind: StarGiftCollectibleModel, Name: "Regular Two", RarityKind: StarGiftRarityPermille, RarityPermille: 78, Animation: animation},
{Kind: StarGiftCollectibleModel, Name: "Crafted", RarityKind: StarGiftRarityLegendary, Crafted: true, Animation: animation},
},
Patterns: []StarGiftCollectibleAttribute{
{Kind: StarGiftCollectiblePattern, Name: "Pattern", RarityKind: StarGiftRarityPermille, RarityPermille: 989, Animation: animation},
{Kind: StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: StarGiftRarityPermille, RarityPermille: 11, Animation: animation},
},
Backdrops: []StarGiftCollectibleAttribute{
{Kind: StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 0, RarityKind: StarGiftRarityPermille, RarityPermille: 999},
{Kind: StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 1, RarityKind: StarGiftRarityPermille, RarityPermille: 1},
},
}
}
@ -103,15 +106,59 @@ func storedCollectibleWrite() StarGiftCollectibleWrite {
}
write.Models[i].Blob = &FileBlob{LocationKey: "model"}
}
write.Patterns[0].Document = &Document{
ID: 200, MimeType: "application/x-tgsticker",
Attributes: []DocumentAttribute{{Kind: DocAttrCustomEmoji, Alt: "🎁", TextColor: true}},
Thumbs: []PhotoSize{{Kind: PhotoSizeKindPath, Type: "j", Bytes: []byte{1}}},
for i := range write.Patterns {
write.Patterns[i].Document = &Document{
ID: int64(200 + i), MimeType: "application/x-tgsticker",
Attributes: []DocumentAttribute{{Kind: DocAttrCustomEmoji, Alt: "🎁", TextColor: true}},
Thumbs: []PhotoSize{{Kind: PhotoSizeKindPath, Type: "j", Bytes: []byte{1}}},
}
write.Patterns[i].Blob = &FileBlob{LocationKey: "pattern"}
}
write.Patterns[0].Blob = &FileBlob{LocationKey: "pattern"}
return write
}
func TestValidateStarGiftCollectibleDraftRequiresClientSafePreviewPool(t *testing.T) {
tests := map[string]func(*StarGiftCollectibleWrite){
"one selectable model": func(write *StarGiftCollectibleWrite) {
write.Models = append(write.Models[:1], write.Models[2:]...)
},
"one selectable pattern": func(write *StarGiftCollectibleWrite) {
write.Patterns = write.Patterns[:1]
},
"one selectable backdrop": func(write *StarGiftCollectibleWrite) {
write.Backdrops = write.Backdrops[:1]
},
"duplicate backdrop id": func(write *StarGiftCollectibleWrite) {
write.Backdrops[1].BackdropID = write.Backdrops[0].BackdropID
},
}
for name, mutate := range tests {
t.Run(name, func(t *testing.T) {
write := validCollectibleDraft()
mutate(&write)
if err := ValidateStarGiftCollectibleDraft(write); !errors.Is(err, ErrStarGiftCollectibleInvalid) {
t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err)
}
})
}
}
func TestValidateStarGiftCollectibleWriteRequiresDistinctPreviewDocuments(t *testing.T) {
for _, kind := range []StarGiftCollectibleAttributeKind{StarGiftCollectibleModel, StarGiftCollectiblePattern} {
t.Run(string(kind), func(t *testing.T) {
write := storedCollectibleWrite()
if kind == StarGiftCollectibleModel {
write.Models[1].Document = write.Models[0].Document
} else {
write.Patterns[1].Document = write.Patterns[0].Document
}
if err := ValidateStarGiftCollectibleWrite(write); !errors.Is(err, ErrStarGiftCollectibleInvalid) {
t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err)
}
})
}
}
func TestValidateStarGiftCollectibleWriteRequiresExactDocumentRoles(t *testing.T) {
if err := ValidateStarGiftCollectibleWrite(storedCollectibleWrite()); err != nil {
t.Fatalf("valid stored collectible: %v", err)

View file

@ -594,16 +594,19 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
t.Fatalf("gift service = %T", r.deps.Gifts)
}
model := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8101, "Aurora")
modelTwo := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8104, "Aurora Two")
crafted := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8103, "Crafted Aurora")
crafted.Crafted = true
crafted.RarityKind = domain.StarGiftRarityLegendary
crafted.RarityPermille = 0
pattern := collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8102, "Orbit")
patternTwo := collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8105, "Orbit Two")
backdrop := collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 1, "Midnight")
backdropTwo := collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 2, "Daylight")
if _, err := giftService.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 500, SlugPrefix: "cake",
Models: []domain.StarGiftCollectibleAttribute{model, crafted}, Patterns: []domain.StarGiftCollectibleAttribute{pattern},
Backdrops: []domain.StarGiftCollectibleAttribute{backdrop}, Actor: "test", CommandID: "collectible-rpc",
Models: []domain.StarGiftCollectibleAttribute{model, crafted, modelTwo}, Patterns: []domain.StarGiftCollectibleAttribute{pattern, patternTwo},
Backdrops: []domain.StarGiftCollectibleAttribute{backdrop, backdropTwo}, Actor: "test", CommandID: "collectible-rpc",
}); err != nil {
t.Fatalf("publish collectible pool: %v", err)
}
@ -615,11 +618,11 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
}
preview, err := r.onPaymentsGetStarGiftUpgradePreview(ownerCtx, gift.ID)
if err != nil || len(preview.SampleAttributes) != 3 {
if err != nil || len(preview.SampleAttributes) != 6 {
t.Fatalf("upgrade preview = %#v err %v", preview, err)
}
attributes, err := r.onPaymentsGetStarGiftUpgradeAttributes(ownerCtx, gift.ID)
if err != nil || len(attributes.Attributes) != 4 {
if err != nil || len(attributes.Attributes) != 7 {
t.Fatalf("upgrade attributes = %#v err %v", attributes, err)
}
craftedTG, ok := attributes.Attributes[1].(*tg.StarGiftAttributeModel)
@ -1004,10 +1007,19 @@ func TestStarGiftChannelSaga(t *testing.T) {
giftService := r.deps.Gifts.(*appstargifts.Service)
if _, err := giftService.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 10, SlugPrefix: "channel-cake",
Models: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8201, "Aurora")},
Patterns: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8202, "Orbit")},
Backdrops: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 2, "Midnight")},
Actor: "test", CommandID: "channel-collectible-rpc",
Models: []domain.StarGiftCollectibleAttribute{
collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8201, "Aurora"),
collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8204, "Aurora Two"),
},
Patterns: []domain.StarGiftCollectibleAttribute{
collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8202, "Orbit"),
collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8205, "Orbit Two"),
},
Backdrops: []domain.StarGiftCollectibleAttribute{
collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 2, "Midnight"),
collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 3, "Daylight"),
},
Actor: "test", CommandID: "channel-collectible-rpc",
}); err != nil {
t.Fatalf("publish channel collectible pool: %v", err)
}

View file

@ -43,24 +43,29 @@ func TestStarGiftCollectibleUpgradeAggregatePostgres(t *testing.T) {
Document: collectibleTestDocumentPtr(baseDocumentID+3, "crafted-model.tgs"),
Blob: collectibleTestBlobPtr(baseDocumentID+3, "crafted-model"), Animation: collectibleTestAnimationPtr("crafted-model.tgs"),
OfficialDocumentID: 5100000000000000003,
}, {
Kind: domain.StarGiftCollectibleModel, Name: "Solar", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 78,
Document: collectibleTestDocumentPtr(baseDocumentID+4, "model-two.tgs"),
Blob: collectibleTestBlobPtr(baseDocumentID+4, "model-two"), Animation: collectibleTestAnimationPtr("model-two.tgs"),
OfficialDocumentID: 5100000000000000004,
}},
Patterns: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 989,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "pattern.tgs"),
Blob: collectibleTestBlobPtr(baseDocumentID+2, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs"),
}},
Backdrops: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectibleBackdrop, Name: "Midnight", BackdropID: 1,
CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff,
RarityKind: domain.StarGiftRarityPermille, RarityPermille: 999,
}},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 989,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")},
{Kind: domain.StarGiftCollectiblePattern, Name: "Rings", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 11,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "pattern-two"), Animation: collectibleTestAnimationPtr("pattern-two.tgs")},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Midnight", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 999},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Daylight", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1},
},
Actor: "integration", CommandID: "collectibles-" + suffix,
OfficialGiftID: 5170145012310081615, SourceManifestSHA256: make([]byte, 32),
})
if err != nil {
t.Fatalf("publish collectible pool: %v", err)
}
if !poolRevision.Published || poolRevision.Issued != 0 || len(poolRevision.Models) != 2 || len(poolRevision.Patterns) != 1 || len(poolRevision.Backdrops) != 1 ||
if !poolRevision.Published || poolRevision.Issued != 0 || len(poolRevision.Models) != 3 || len(poolRevision.Patterns) != 2 || len(poolRevision.Backdrops) != 2 ||
!poolRevision.Models[1].Crafted || poolRevision.Models[1].RarityKind != domain.StarGiftRarityLegendary ||
poolRevision.Models[1].RarityPermille != 0 || poolRevision.Models[0].OfficialDocumentID != 5100000000000000001 {
t.Fatalf("published pool = %+v", poolRevision)
@ -353,21 +358,22 @@ WHERE owner_user_id=$1 AND msg_id=$2`, sender.ID, ownerMessage.ID).Scan(&senderA
}
soldOutRevision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
GiftID: soldOutEntry.Gift.ID, UpgradeStars: 10, SupplyTotal: 1, SlugPrefix: "nova-" + suffix,
Models: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectibleModel, Name: "Nova", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestDocumentPtr(baseDocumentID+101, "nova-model.tgs"),
Blob: collectibleTestBlobPtr(baseDocumentID+101, "nova-model"), Animation: collectibleTestAnimationPtr("nova-model.tgs"),
}},
Patterns: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectiblePattern, Name: "Ray", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+102, "nova-pattern.tgs"),
Blob: collectibleTestBlobPtr(baseDocumentID+102, "nova-pattern"), Animation: collectibleTestAnimationPtr("nova-pattern.tgs"),
}},
Backdrops: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectibleBackdrop, Name: "Void", BackdropID: 2,
CenterColor: 0x101010, EdgeColor: 0x202020, PatternColor: 0x303030, TextColor: 0xffffff,
RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
}},
Models: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleModel, Name: "Nova", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestDocumentPtr(baseDocumentID+101, "nova-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+101, "nova-model"), Animation: collectibleTestAnimationPtr("nova-model.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Nova Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestDocumentPtr(baseDocumentID+103, "nova-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+103, "nova-model-two"), Animation: collectibleTestAnimationPtr("nova-model-two.tgs")},
},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Ray", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+102, "nova-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+102, "nova-pattern"), Animation: collectibleTestAnimationPtr("nova-pattern.tgs")},
{Kind: domain.StarGiftCollectiblePattern, Name: "Ray Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+104, "nova-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+104, "nova-pattern-two"), Animation: collectibleTestAnimationPtr("nova-pattern-two.tgs")},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Void", BackdropID: 2, CenterColor: 0x101010, EdgeColor: 0x202020, PatternColor: 0x303030, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Light", BackdropID: 3, CenterColor: 0xeeeeee, EdgeColor: 0xcccccc, PatternColor: 0xaaaaaa, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
},
Actor: "integration", CommandID: "soldout-pool-" + suffix,
})
if err != nil {
@ -437,6 +443,65 @@ WHERE owner_user_id=$1 AND msg_id=$2`, sender.ID, ownerMessage.ID).Scan(&senderA
}
}
func TestStarGiftCollectiblePreviewActivationGuardPostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000
gifts := NewStarGiftStore(pool)
entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
Title: "Unsafe Preview " + suffix, Stars: 10, ConvertStars: 5, Enabled: true,
Document: collectibleTestDocument(baseDocumentID, "unsafe-preview.tgs"), Blob: collectibleTestBlob(baseDocumentID, "unsafe-preview"),
Animation: collectibleTestAnimation("unsafe-preview.tgs"), Actor: "integration", CommandID: "unsafe-preview-catalog-" + suffix,
})
if err != nil {
t.Fatalf("create unsafe-preview catalog: %v", err)
}
var revisionID int64
if err := pool.QueryRow(ctx, `
INSERT INTO star_gift_collectible_revisions
(gift_id, revision, upgrade_stars, supply_total, slug_prefix, status, created_by, command_id)
VALUES ($1, 1, 10, 10, $2, 'draft', 'integration', $3)
RETURNING id`, entry.Gift.ID, "unsafe-"+suffix, "unsafe-preview-pool-"+suffix).Scan(&revisionID); err != nil {
t.Fatalf("insert unsafe-preview revision: %v", err)
}
animation := collectibleTestAnimation("unsafe-preview-attribute.tgs")
if _, err := pool.Exec(ctx, `
INSERT INTO star_gift_collectible_models
(collectible_revision_id, name, document_id, animation_json, animation_sha256, source_name, source_format,
width, height, frame_rate, in_point, out_point, rarity_kind, rarity_permille, crafted, sort_order)
VALUES ($1, 'Only Model', $2, $3::jsonb, $4, 'model.tgs', 'tgs', 512, 512, 30, 0, 60, 'permille', 1000, false, 0)`,
revisionID, baseDocumentID, string(animation.JSON), animation.SHA256); err != nil {
t.Fatalf("insert unsafe-preview model: %v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO star_gift_collectible_patterns
(collectible_revision_id, name, document_id, animation_json, animation_sha256, source_name, source_format,
width, height, frame_rate, in_point, out_point, rarity_kind, rarity_permille, sort_order)
VALUES ($1, 'Only Pattern', $2, $3::jsonb, $4, 'pattern.tgs', 'tgs', 512, 512, 30, 0, 60, 'permille', 1000, 0)`,
revisionID, baseDocumentID, string(animation.JSON), animation.SHA256); err != nil {
t.Fatalf("insert unsafe-preview pattern: %v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO star_gift_collectible_backdrops
(collectible_revision_id, name, backdrop_id, center_color, edge_color, pattern_color, text_color,
rarity_kind, rarity_permille, sort_order)
VALUES ($1, 'Only Backdrop', 1, 1, 2, 3, 4, 'permille', 1000, 0)`, revisionID); err != nil {
t.Fatalf("insert unsafe-preview backdrop: %v", err)
}
if _, err := pool.Exec(ctx, `
UPDATE star_gift_collectible_revisions SET status='published', published_at=now() WHERE id=$1`, revisionID); err != nil {
t.Fatalf("publish unsafe-preview revision directly: %v", err)
}
if _, err := pool.Exec(ctx, `UPDATE star_gift_catalog SET collectible_revision_id=$2 WHERE gift_id=$1`, entry.Gift.ID, revisionID); err == nil {
t.Fatal("database activated a collectible preview pool with one client-distinct item per category")
}
var activeRevisionID *int64
if err := pool.QueryRow(ctx, `SELECT collectible_revision_id FROM star_gift_catalog WHERE gift_id=$1`, entry.Gift.ID).Scan(&activeRevisionID); err != nil || activeRevisionID != nil {
t.Fatalf("unsafe preview activation pointer=%v err=%v, want null", activeRevisionID, err)
}
}
func TestStarGiftUpgradeWithoutCraftedModelDoesNotAdvertiseCraft(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
@ -460,27 +525,28 @@ func TestStarGiftUpgradeWithoutCraftedModelDoesNotAdvertiseCraft(t *testing.T) {
}
revision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "no-craft-" + suffix,
Models: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectibleModel, Name: "Ordinary", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestDocumentPtr(baseDocumentID+1, "no-craft-model.tgs"),
Blob: collectibleTestBlobPtr(baseDocumentID+1, "no-craft-model"), Animation: collectibleTestAnimationPtr("no-craft-model.tgs"),
}},
Patterns: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "no-craft-pattern.tgs"),
Blob: collectibleTestBlobPtr(baseDocumentID+2, "no-craft-pattern"), Animation: collectibleTestAnimationPtr("no-craft-pattern.tgs"),
}},
Backdrops: []domain.StarGiftCollectibleAttribute{{
Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 1,
CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff,
RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
}},
Models: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleModel, Name: "Ordinary", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestDocumentPtr(baseDocumentID+1, "no-craft-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "no-craft-model"), Animation: collectibleTestAnimationPtr("no-craft-model.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Ordinary Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestDocumentPtr(baseDocumentID+3, "no-craft-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "no-craft-model-two"), Animation: collectibleTestAnimationPtr("no-craft-model-two.tgs")},
},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "no-craft-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "no-craft-pattern"), Animation: collectibleTestAnimationPtr("no-craft-pattern.tgs")},
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+4, "no-craft-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "no-craft-pattern-two"), Animation: collectibleTestAnimationPtr("no-craft-pattern-two.tgs")},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
},
Actor: "integration", CommandID: "no-craft-pool-" + suffix,
})
if err != nil {
t.Fatalf("publish no-craft pool: %v", err)
}
if len(revision.Models) != 1 || revision.Models[0].Crafted {
if len(revision.Models) != 2 || revision.Models[0].Crafted || revision.Models[1].Crafted {
t.Fatalf("no-craft pool models = %+v", revision.Models)
}

View file

@ -48,12 +48,19 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) {
Document: collectibleTestDocumentPtr(baseDocumentID+1, "model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "model"), Animation: collectibleTestAnimationPtr("model.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true,
Document: collectibleTestDocumentPtr(baseDocumentID+2, "crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "crafted"), Animation: collectibleTestAnimationPtr("crafted.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Base Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestDocumentPtr(baseDocumentID+4, "model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "model-two"), Animation: collectibleTestAnimationPtr("model-two.tgs")},
},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")},
{Kind: domain.StarGiftCollectiblePattern, Name: "Orbit Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "pattern-two"), Animation: collectibleTestAnimationPtr("pattern-two.tgs")},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Night", BackdropID: 77, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Day", BackdropID: 78, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000},
},
Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}},
Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Night", BackdropID: 77,
CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff,
RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}},
Actor: "integration", CommandID: "lifecycle-pool-" + suffix,
}); err != nil {
t.Fatalf("publish lifecycle pool: %v", err)
@ -760,12 +767,19 @@ func TestStarGiftChannelLifecycleAtomicPostgres(t *testing.T) {
Document: collectibleTestDocumentPtr(baseDocumentID+1, "channel-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "channel-model"), Animation: collectibleTestAnimationPtr("channel-model.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Channel Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true,
Document: collectibleTestDocumentPtr(baseDocumentID+2, "channel-crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "channel-crafted"), Animation: collectibleTestAnimationPtr("channel-crafted.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Channel Model Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestDocumentPtr(baseDocumentID+4, "channel-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "channel-model-two"), Animation: collectibleTestAnimationPtr("channel-model-two.tgs")},
},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Channel Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "channel-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "channel-pattern"), Animation: collectibleTestAnimationPtr("channel-pattern.tgs")},
{Kind: domain.StarGiftCollectiblePattern, Name: "Channel Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "channel-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "channel-pattern-two"), Animation: collectibleTestAnimationPtr("channel-pattern-two.tgs")},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Channel Backdrop", BackdropID: 88, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Channel Backdrop Two", BackdropID: 89, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000},
},
Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Channel Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "channel-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "channel-pattern"), Animation: collectibleTestAnimationPtr("channel-pattern.tgs")}},
Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Channel Backdrop", BackdropID: 88,
CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff,
RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}},
Actor: "integration", CommandID: "channel-gift-pool-" + suffix,
}); err != nil {
t.Fatalf("publish channel gift pool: %v", err)
@ -1088,13 +1102,19 @@ func TestStarGiftCraftFailureConsumesThreeInputsPostgres(t *testing.T) {
Document: collectibleTestDocumentPtr(baseDocumentID+1, "base.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "base"), Animation: collectibleTestAnimationPtr("base.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true,
Document: collectibleTestDocumentPtr(baseDocumentID+2, "crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "crafted"), Animation: collectibleTestAnimationPtr("crafted.tgs")},
{Kind: domain.StarGiftCollectibleModel, Name: "Base Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestDocumentPtr(baseDocumentID+4, "base-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "base-two"), Animation: collectibleTestAnimationPtr("base-two.tgs")},
},
Patterns: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")},
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "pattern-two"), Animation: collectibleTestAnimationPtr("pattern-two.tgs")},
},
Backdrops: []domain.StarGiftCollectibleAttribute{
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 88, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000},
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 89, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000},
},
Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern",
RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}},
Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 88,
CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff,
RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}},
Actor: "integration", CommandID: "three-input-pool-" + suffix,
}); err != nil {
t.Fatalf("publish three-input collectible: %v", err)

View file

@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
if err != nil {
t.Fatalf("migrate star gift lifecycle schema: %v", err)
}
if status.Dirty || status.Empty || status.Version != 129 {
t.Fatalf("migration status = %+v, want clean version 129", status)
if status.Dirty || status.Empty || status.Version != 132 {
t.Fatalf("migration status = %+v, want clean version 132", status)
}
}

View file

@ -22,7 +22,7 @@ func TestOfficialStarGiftBundleIsAtomicPostgres(t *testing.T) {
attribute := func(kind domain.StarGiftCollectibleAttributeKind, id int64, name string) domain.StarGiftCollectibleAttribute {
value := domain.StarGiftCollectibleAttribute{Kind: kind, Name: name, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 918}
if kind == domain.StarGiftCollectibleBackdrop {
value.BackdropID = 0
value.BackdropID = int(id)
value.CenterColor, value.EdgeColor, value.PatternColor, value.TextColor = 1, 2, 3, 4
return value
}
@ -46,10 +46,19 @@ func TestOfficialStarGiftBundleIsAtomicPostgres(t *testing.T) {
},
Collectible: &domain.StarGiftCollectibleWrite{
UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "official-" + suffix,
Models: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectibleModel, baseID+1, "model")},
Patterns: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectiblePattern, baseID+2, "pattern")},
Backdrops: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop")},
Actor: "integration", CommandID: "official-pool-" + suffix,
Models: []domain.StarGiftCollectibleAttribute{
attribute(domain.StarGiftCollectibleModel, baseID+1, "model"),
attribute(domain.StarGiftCollectibleModel, baseID+3, "model-two"),
},
Patterns: []domain.StarGiftCollectibleAttribute{
attribute(domain.StarGiftCollectiblePattern, baseID+2, "pattern"),
attribute(domain.StarGiftCollectiblePattern, baseID+4, "pattern-two"),
},
Backdrops: []domain.StarGiftCollectibleAttribute{
attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop"),
attribute(domain.StarGiftCollectibleBackdrop, 1, "backdrop-two"),
},
Actor: "integration", CommandID: "official-pool-" + suffix,
OfficialGiftID: 5170145012310081615, SourceManifestSHA256: manifestSHA,
},
}
@ -81,9 +90,15 @@ FROM star_gift_catalog_revisions WHERE id=$1`, result.Catalog.Gift.RevisionID).S
attribute(domain.StarGiftCollectibleModel, baseID+101, "duplicate"),
attribute(domain.StarGiftCollectibleModel, baseID+102, "duplicate"),
},
Patterns: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectiblePattern, baseID+103, "pattern")},
Backdrops: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop")},
Actor: "integration", CommandID: "rollback-pool-" + suffix,
Patterns: []domain.StarGiftCollectibleAttribute{
attribute(domain.StarGiftCollectiblePattern, baseID+103, "pattern"),
attribute(domain.StarGiftCollectiblePattern, baseID+104, "pattern-two"),
},
Backdrops: []domain.StarGiftCollectibleAttribute{
attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop"),
attribute(domain.StarGiftCollectibleBackdrop, 1, "backdrop-two"),
},
Actor: "integration", CommandID: "rollback-pool-" + suffix,
}
if _, err := store.CreateCatalogBundle(ctx, failing); !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) {
t.Fatalf("failing bundle err=%v", err)