CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 24s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 1m44s
CI / Build and push Docker image (push) Successful in 3m2s
CI / Notify (push) Successful in 2s
529 lines
21 KiB
TypeScript
529 lines
21 KiB
TypeScript
import { resetMemoryStorage } from '../storage/memory';
|
||
import getStorage from '../storage';
|
||
import {
|
||
getStats,
|
||
recordCatches,
|
||
repairNet,
|
||
killWasp,
|
||
buyRepellent,
|
||
reportNetTorn,
|
||
robbery,
|
||
defeatThief,
|
||
caterpillarAte,
|
||
defeatCaterpillar,
|
||
mothHit,
|
||
reportInspectionFail,
|
||
buyPremium,
|
||
waspSpray,
|
||
buyInsurance,
|
||
buyUpgrade,
|
||
getAchievements,
|
||
upgradeCost,
|
||
getLeaderboard,
|
||
growPests,
|
||
levelForCaught,
|
||
levelThreshold,
|
||
levelProgress,
|
||
titleForLevel,
|
||
comboBonus,
|
||
InsufficientCoinsError,
|
||
COIN_BASE,
|
||
GOLD_VALUE,
|
||
REPAIR_COST,
|
||
DAILY_BONUS,
|
||
PREMIUM_MILESTONE,
|
||
WASP_MAX,
|
||
BIRD_MAX,
|
||
WASP_GROWTH_MS,
|
||
BIRD_GROWTH_MS,
|
||
REPELLENT_COST,
|
||
REPELLENT_DURATION_MS,
|
||
RATE_CAP_PER_MIN,
|
||
MAX_LEVEL,
|
||
ROBBERY_FRACTION,
|
||
THIEF_REWARD,
|
||
CATERPILLAR_EATS,
|
||
CATERPILLAR_REWARD,
|
||
AUTO_REPAIR_MS,
|
||
BAN_FAIL_THRESHOLD,
|
||
BAN_DURATION_MS,
|
||
BAN_WINDOW_MS,
|
||
MOTH_PENALTY,
|
||
PREMIUM_BUY_COST,
|
||
WASP_SPRAY_COST,
|
||
INSURANCE_COST,
|
||
INSURANCE_DURATION_MS,
|
||
ROBBERY_FRACTION_INSURED,
|
||
UPGRADE_MAX,
|
||
DAILY_TASK_REWARD,
|
||
} from '../butterflies';
|
||
import { formatDate } from '../utils';
|
||
|
||
const USER = 'tomas';
|
||
const OTHER = 'petr';
|
||
/** Storage klíč sezóny 2 (musí odpovídat STORAGE_KEY v butterflies.ts). */
|
||
const KEY = 'butterflyStats_s2';
|
||
|
||
beforeEach(() => {
|
||
resetMemoryStorage();
|
||
});
|
||
|
||
/** Nastaví uživateli přímo uložené statistiky (simulace zásahu do dat). */
|
||
async function seed(login: string, data: any) {
|
||
const storage = getStorage();
|
||
const all = (await storage.getData<any>(KEY)) ?? {};
|
||
all[login] = data;
|
||
await storage.setData(KEY, all);
|
||
}
|
||
|
||
describe('úrovně', () => {
|
||
test('začíná na úrovni 1', () => {
|
||
expect(levelForCaught(0)).toBe(1);
|
||
expect(titleForLevel(1)).toBe('Začátečník se síťkou');
|
||
});
|
||
|
||
test('křivka je rostoucí a stále strmější', () => {
|
||
expect(levelThreshold(1)).toBe(0);
|
||
expect(levelThreshold(2)).toBeGreaterThan(0);
|
||
// každý další skok je větší než ten předchozí (těžší postup)
|
||
const d1 = levelThreshold(3) - levelThreshold(2);
|
||
const d2 = levelThreshold(4) - levelThreshold(3);
|
||
const d3 = levelThreshold(20) - levelThreshold(19);
|
||
expect(d2).toBeGreaterThan(d1);
|
||
expect(d3).toBeGreaterThan(d2);
|
||
});
|
||
|
||
test('levelForCaught respektuje prahy a strop', () => {
|
||
expect(levelForCaught(levelThreshold(2) - 1)).toBe(1);
|
||
expect(levelForCaught(levelThreshold(2))).toBe(2);
|
||
expect(levelForCaught(levelThreshold(5))).toBe(5);
|
||
expect(levelForCaught(100_000_000)).toBe(MAX_LEVEL);
|
||
});
|
||
|
||
test('titul nad rámec seznamu dostane hvězdičky (prestiž)', () => {
|
||
expect(titleForLevel(MAX_LEVEL)).toContain('★');
|
||
});
|
||
|
||
test('levelProgress je 0–100 a na maximu 100', () => {
|
||
expect(levelProgress(0)).toBe(0);
|
||
const p = levelProgress(Math.round((levelThreshold(2) + levelThreshold(3)) / 2));
|
||
expect(p).toBeGreaterThan(0);
|
||
expect(p).toBeLessThan(100);
|
||
expect(levelProgress(100_000_000)).toBe(100);
|
||
});
|
||
});
|
||
|
||
describe('comboBonus', () => {
|
||
test('malá dávka nedává kombo', () => {
|
||
expect(comboBonus(1)).toBe(0);
|
||
expect(comboBonus(3)).toBe(0); // pod prahem 4
|
||
});
|
||
|
||
test('větší dávka dává rostoucí bonus se stropem', () => {
|
||
expect(comboBonus(4)).toBe(1);
|
||
expect(comboBonus(5)).toBe(2);
|
||
expect(comboBonus(1000)).toBe(8); // COMBO_CAP
|
||
});
|
||
});
|
||
|
||
describe('getStats', () => {
|
||
test('pro nového uživatele vrací výchozí prázdné statistiky', async () => {
|
||
const stats = await getStats(USER);
|
||
expect(stats.caught).toBe(0);
|
||
expect(stats.coins).toBe(0);
|
||
expect(stats.goldenCaught).toBe(0);
|
||
expect(stats.level).toBe(1);
|
||
expect(stats.title).toBe('Začátečník se síťkou');
|
||
expect(stats.wasps).toBe(0);
|
||
expect(stats.birds).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('recordCatches – mince', () => {
|
||
test('běžný motýl dá základní minci + denní bonus', async () => {
|
||
const result = await recordCatches(USER, 1, 0);
|
||
expect(result.coinsAwarded).toBe(COIN_BASE + DAILY_BONUS);
|
||
expect(result.dailyBonusApplied).toBe(true);
|
||
expect(result.stats.caught).toBe(1);
|
||
expect(result.stats.coins).toBe(COIN_BASE + DAILY_BONUS);
|
||
});
|
||
|
||
test('zlatý motýl má vyšší hodnotu a počítá se zvlášť', async () => {
|
||
await seed(USER, { caught: 5, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||
const result = await recordCatches(USER, 0, 1);
|
||
expect(result.coinsAwarded).toBe(GOLD_VALUE);
|
||
expect(result.dailyBonusApplied).toBe(false);
|
||
expect(result.stats.goldenCaught).toBe(1);
|
||
expect(result.stats.caught).toBe(6);
|
||
});
|
||
|
||
test('kombo se připočítá u větší dávky', async () => {
|
||
await seed(USER, { caught: 5, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||
const result = await recordCatches(USER, 5, 0);
|
||
expect(result.coinsAwarded).toBe(5 * COIN_BASE + comboBonus(5));
|
||
});
|
||
|
||
test('sanity limit + rate cap ořízne absurdní dávku', async () => {
|
||
const result = await recordCatches(USER, 100000, 0);
|
||
// MAX_BATCH=100, ale rate cap povolí jen RATE_CAP_PER_MIN za minutu
|
||
expect(result.stats.caught).toBe(RATE_CAP_PER_MIN);
|
||
});
|
||
});
|
||
|
||
describe('recordCatches – rate cap (ochrana proti podvádění)', () => {
|
||
test('v jednom okně se započte max RATE_CAP_PER_MIN, zbytek se zahodí', async () => {
|
||
const t0 = 1_000_000;
|
||
const a = await recordCatches(USER, 60, 0, t0);
|
||
expect(a.stats.caught).toBe(60);
|
||
const b = await recordCatches(USER, 60, 0, t0 + 1000);
|
||
expect(b.stats.caught).toBe(RATE_CAP_PER_MIN); // 60 + 30
|
||
const c = await recordCatches(USER, 20, 0, t0 + 2000);
|
||
expect(c.stats.caught).toBe(RATE_CAP_PER_MIN); // už nic nepřibude
|
||
expect(c.coinsAwarded).toBe(0);
|
||
});
|
||
|
||
test('po uplynutí okna se limit resetuje', async () => {
|
||
const t0 = 2_000_000;
|
||
await recordCatches(USER, RATE_CAP_PER_MIN, 0, t0);
|
||
const later = await recordCatches(USER, 10, 0, t0 + 61_000);
|
||
expect(later.stats.caught).toBe(RATE_CAP_PER_MIN + 10);
|
||
});
|
||
});
|
||
|
||
describe('recordCatches – denní bonus', () => {
|
||
test('denní bonus se připíše jen jednou za den', async () => {
|
||
const first = await recordCatches(USER, 1, 0);
|
||
expect(first.dailyBonusApplied).toBe(true);
|
||
const second = await recordCatches(USER, 1, 0);
|
||
expect(second.dailyBonusApplied).toBe(false);
|
||
expect(second.coinsAwarded).toBe(COIN_BASE);
|
||
});
|
||
|
||
test('bonus se znovu připíše po změně dne', async () => {
|
||
await seed(USER, { caught: 3, coins: 3, goldenCaught: 0, lastCatchDay: '2000-01-01' });
|
||
const result = await recordCatches(USER, 1, 0);
|
||
expect(result.dailyBonusApplied).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('recordCatches – progres a milníky', () => {
|
||
test('leveledUp se nastaví při překročení hranice úrovně', async () => {
|
||
await seed(USER, { caught: levelThreshold(2) - 1, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||
const result = await recordCatches(USER, 1, 0);
|
||
expect(result.leveledUp).toBe(true);
|
||
expect(result.stats.level).toBe(2);
|
||
});
|
||
|
||
test('premiumUnlocked se nastaví při překročení milníku', async () => {
|
||
await seed(USER, { caught: PREMIUM_MILESTONE - 1, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||
const result = await recordCatches(USER, 1, 0);
|
||
expect(result.premiumUnlocked).toBe(true);
|
||
});
|
||
|
||
test('bez překročení milníku premiumUnlocked zůstane false', async () => {
|
||
await seed(USER, { caught: 5, coins: 0, goldenCaught: 0, lastCatchDay: formatDate(new Date()) });
|
||
const result = await recordCatches(USER, 1, 0);
|
||
expect(result.premiumUnlocked).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('growPests', () => {
|
||
test('vosy přibývají v čase do maxima', () => {
|
||
const s: any = { wasps: 0, birds: 0, repellentUntil: 0, lastWaspAt: 0, lastBirdAt: 0 };
|
||
growPests(s, 2 * WASP_GROWTH_MS + 100);
|
||
expect(s.wasps).toBe(2);
|
||
growPests(s, 1_000_000 * WASP_GROWTH_MS);
|
||
expect(s.wasps).toBe(WASP_MAX);
|
||
});
|
||
|
||
test('zachovává rozpracovaný čas (dvě poloviční okna = jedna vosa)', () => {
|
||
const s: any = { wasps: 0, birds: 0, repellentUntil: 0, lastWaspAt: 0, lastBirdAt: 0 };
|
||
growPests(s, Math.floor(WASP_GROWTH_MS * 0.6));
|
||
expect(s.wasps).toBe(0);
|
||
growPests(s, Math.floor(WASP_GROWTH_MS * 1.2));
|
||
expect(s.wasps).toBe(1);
|
||
});
|
||
|
||
test('ptáci nepřibývají při aktivním plašiči', () => {
|
||
const now = 1_000_000;
|
||
const s: any = { wasps: 0, birds: 0, repellentUntil: now + 10 * BIRD_GROWTH_MS, lastWaspAt: now, lastBirdAt: now };
|
||
growPests(s, now + 3 * BIRD_GROWTH_MS);
|
||
expect(s.birds).toBe(0);
|
||
});
|
||
|
||
test('ptáci přibývají do maxima', () => {
|
||
const s: any = { wasps: 0, birds: 0, repellentUntil: 0, lastWaspAt: 0, lastBirdAt: 0 };
|
||
growPests(s, 1_000_000 * BIRD_GROWTH_MS);
|
||
expect(s.birds).toBe(BIRD_MAX);
|
||
});
|
||
});
|
||
|
||
describe('killWasp', () => {
|
||
test('sníží počet vos a zvýší statistiku zahubených', async () => {
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, wasps: 3, lastWaspAt: Date.now() });
|
||
const stats = await killWasp(USER);
|
||
expect(stats.wasps).toBe(2);
|
||
expect(stats.waspsKilled).toBe(1);
|
||
});
|
||
|
||
test('při nula vosách nic nepokazí', async () => {
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, wasps: 0, lastWaspAt: Date.now() });
|
||
const stats = await killWasp(USER);
|
||
expect(stats.wasps).toBe(0);
|
||
expect(stats.waspsKilled).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('buyRepellent', () => {
|
||
test('vyžene ptáky, aktivuje plašič a odečte mince', async () => {
|
||
const now = 5_000_000;
|
||
await seed(USER, { caught: 0, coins: REPELLENT_COST + 3, goldenCaught: 0, birds: 3, lastBirdAt: now });
|
||
const stats = await buyRepellent(USER, now);
|
||
expect(stats.coins).toBe(3);
|
||
expect(stats.birds).toBe(0);
|
||
expect(stats.birdsScared).toBe(3);
|
||
expect(stats.repellentUntil).toBe(now + REPELLENT_DURATION_MS);
|
||
});
|
||
|
||
test('vyhodí chybu při nedostatku mincí', async () => {
|
||
await seed(USER, { caught: 0, coins: REPELLENT_COST - 1, goldenCaught: 0, birds: 2 });
|
||
await expect(buyRepellent(USER)).rejects.toThrow(InsufficientCoinsError);
|
||
});
|
||
|
||
test('po vypršení plašiče ptáci zase přibývají', async () => {
|
||
const t0 = 10_000_000;
|
||
await seed(USER, { caught: 0, coins: REPELLENT_COST, goldenCaught: 0, birds: 0, lastBirdAt: t0 });
|
||
await buyRepellent(USER, t0);
|
||
// dost dlouho po vypršení plašiče
|
||
const stats = await getStats(USER, t0 + REPELLENT_DURATION_MS + 2 * BIRD_GROWTH_MS + 1);
|
||
expect(stats.birds).toBeGreaterThanOrEqual(2);
|
||
});
|
||
});
|
||
|
||
describe('reportNetTorn', () => {
|
||
test('protrhne síťku na dobu auto-opravy', async () => {
|
||
const now = 1_000_000;
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, netTornUntil: 0 });
|
||
const stats = await reportNetTorn(USER, now);
|
||
expect(stats.netTornUntil).toBe(now + AUTO_REPAIR_MS);
|
||
});
|
||
|
||
test('opakované protržení neprodlouží běžící odpočet', async () => {
|
||
const now = 1_000_000;
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, netTornUntil: now + 40_000 });
|
||
const stats = await reportNetTorn(USER, now);
|
||
expect(stats.netTornUntil).toBe(now + 40_000);
|
||
});
|
||
|
||
test('sama se zašije po uplynutí odpočtu', async () => {
|
||
const now = 1_000_000;
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, netTornUntil: now + 1000 });
|
||
const stats = await getStats(USER, now + 2000);
|
||
expect(stats.netTornUntil).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('repairNet', () => {
|
||
test('okamžitá oprava odečte cenu a zašije síťku', async () => {
|
||
const now = 1_000_000;
|
||
await seed(USER, { caught: 20, coins: REPAIR_COST + 5, goldenCaught: 0, netTornUntil: now + 30_000 });
|
||
const stats = await repairNet(USER, now);
|
||
expect(stats.coins).toBe(5);
|
||
expect(stats.netTornUntil).toBe(0);
|
||
});
|
||
|
||
test('bez mincí okamžitá oprava selže (ale síťka se sama zašije)', async () => {
|
||
const now = 1_000_000;
|
||
await seed(USER, { caught: 1, coins: REPAIR_COST - 1, goldenCaught: 0, netTornUntil: now + 30_000 });
|
||
await expect(repairNet(USER, now)).rejects.toThrow(InsufficientCoinsError);
|
||
const stats = await getStats(USER, now);
|
||
expect(stats.coins).toBe(REPAIR_COST - 1);
|
||
expect(stats.netTornUntil).toBeGreaterThan(now);
|
||
});
|
||
|
||
test('neúčtuje, když síťka není protržená', async () => {
|
||
const now = 1_000_000;
|
||
await seed(USER, { caught: 1, coins: 50, goldenCaught: 0, netTornUntil: 0 });
|
||
const stats = await repairNet(USER, now);
|
||
expect(stats.coins).toBe(50);
|
||
});
|
||
});
|
||
|
||
describe('zloděj', () => {
|
||
test('okradení sebere podíl mincí', async () => {
|
||
await seed(USER, { caught: 0, coins: 100, goldenCaught: 0 });
|
||
const stats = await robbery(USER);
|
||
expect(stats.coins).toBe(Math.round(100 * (1 - ROBBERY_FRACTION)));
|
||
});
|
||
|
||
test('poražení zloděje dá odměnu a zvýší statistiku', async () => {
|
||
const now = 4_000_000;
|
||
const day = formatDate(new Date(now));
|
||
// Denní úkol pevně nastavíme na jiný druh – jinak by se v některé dny
|
||
// (kdy je úkolem "poraž 1 zloděje") k odměně přičetla i jeho výplata.
|
||
await seed(USER, {
|
||
caught: 0, coins: 10, goldenCaught: 0,
|
||
daily: { day, taskId: 'wasp', progress: 0, target: 15, done: false },
|
||
});
|
||
const stats = await defeatThief(USER, now);
|
||
expect(stats.coins).toBe(10 + THIEF_REWARD);
|
||
expect(stats.thievesDefeated).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('housenka', () => {
|
||
test('sežere část úlovků (nikdy pod nulu) a může snížit úroveň', async () => {
|
||
await seed(USER, { caught: CATERPILLAR_EATS + 5, coins: 0, goldenCaught: 0 });
|
||
const stats = await caterpillarAte(USER);
|
||
expect(stats.caught).toBe(5);
|
||
});
|
||
|
||
test('nesnese caught pod nulu', async () => {
|
||
await seed(USER, { caught: 3, coins: 0, goldenCaught: 0 });
|
||
const stats = await caterpillarAte(USER);
|
||
expect(stats.caught).toBe(0);
|
||
});
|
||
|
||
test('poražení housenky dá odměnu a zvýší statistiku', async () => {
|
||
await seed(USER, { caught: 0, coins: 5, goldenCaught: 0 });
|
||
const stats = await defeatCaterpillar(USER);
|
||
expect(stats.coins).toBe(5 + CATERPILLAR_REWARD);
|
||
expect(stats.caterpillarsDefeated).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('černé můry', () => {
|
||
test('chycení můry ubere úlovky (nikdy pod nulu) a zvýší statistiku', async () => {
|
||
await seed(USER, { caught: MOTH_PENALTY + 4, coins: 0, goldenCaught: 0 });
|
||
const stats = await mothHit(USER);
|
||
expect(stats.caught).toBe(4);
|
||
expect(stats.mothsHit).toBe(1);
|
||
});
|
||
test('nesnese caught pod nulu', async () => {
|
||
await seed(USER, { caught: 2, coins: 0, goldenCaught: 0 });
|
||
const stats = await mothHit(USER);
|
||
expect(stats.caught).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('obchod – spotřební', () => {
|
||
test('buyPremium odečte cenu', async () => {
|
||
await seed(USER, { caught: 0, coins: PREMIUM_BUY_COST + 5, goldenCaught: 0 });
|
||
const stats = await buyPremium(USER);
|
||
expect(stats.coins).toBe(5);
|
||
});
|
||
test('buyPremium bez mincí selže', async () => {
|
||
await seed(USER, { caught: 0, coins: PREMIUM_BUY_COST - 1, goldenCaught: 0 });
|
||
await expect(buyPremium(USER)).rejects.toThrow(InsufficientCoinsError);
|
||
});
|
||
test('waspSpray vyhubí všechny vosy a odečte cenu', async () => {
|
||
await seed(USER, { caught: 0, coins: WASP_SPRAY_COST, goldenCaught: 0, wasps: 4, lastWaspAt: Date.now() });
|
||
const stats = await waspSpray(USER);
|
||
expect(stats.coins).toBe(0);
|
||
expect(stats.wasps).toBe(0);
|
||
});
|
||
test('pojistka sníží ztrátu při okradení a spotřebuje se', async () => {
|
||
const now = 3_000_000;
|
||
await seed(USER, { caught: 0, coins: INSURANCE_COST + 100, goldenCaught: 0 });
|
||
await buyInsurance(USER, now);
|
||
const robbed = await robbery(USER, now + 1000);
|
||
// po koupi zbývá 100 mincí; pojistka → ztráta jen ROBBERY_FRACTION_INSURED
|
||
expect(robbed.coins).toBe(Math.round(100 * (1 - ROBBERY_FRACTION_INSURED)));
|
||
expect(robbed.insuranceUntil).toBe(0); // spotřebováno
|
||
});
|
||
});
|
||
|
||
describe('obchod – trvalá vylepšení', () => {
|
||
test('cena roste s úrovní a vylepšení se aplikuje', async () => {
|
||
await seed(USER, { caught: 0, coins: upgradeCost('net', 0) + upgradeCost('net', 1) + 5, goldenCaught: 0 });
|
||
const a = await buyUpgrade(USER, 'net');
|
||
expect(a.upgrades.net).toBe(1);
|
||
const b = await buyUpgrade(USER, 'net');
|
||
expect(b.upgrades.net).toBe(2);
|
||
expect(b.coins).toBe(5);
|
||
});
|
||
test('nelze koupit nad maximum', async () => {
|
||
await seed(USER, { caught: 0, coins: 1_000_000, goldenCaught: 0, upgrades: { net: UPGRADE_MAX.net, scarecrow: 0, reinforced: 0 } });
|
||
await expect(buyUpgrade(USER, 'net')).rejects.toThrow();
|
||
});
|
||
test('bez mincí koupě selže', async () => {
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0 });
|
||
await expect(buyUpgrade(USER, 'net')).rejects.toThrow(InsufficientCoinsError);
|
||
});
|
||
});
|
||
|
||
describe('denní úkol', () => {
|
||
test('progres se plní a po splnění se vyplatí odměna jednou', async () => {
|
||
const now = 4_000_000;
|
||
const day = formatDate(new Date(now));
|
||
await seed(USER, {
|
||
caught: 0, coins: 0, goldenCaught: 0, wasps: 5, lastWaspAt: now,
|
||
daily: { day, taskId: 'wasp', progress: 0, target: 2, done: false },
|
||
});
|
||
await killWasp(USER, now);
|
||
let stats = await killWasp(USER, now);
|
||
expect(stats.daily.done).toBe(true);
|
||
expect(stats.coins).toBe(DAILY_TASK_REWARD);
|
||
// další zabití už odměnu nepřidá
|
||
stats = await killWasp(USER, now);
|
||
expect(stats.coins).toBe(DAILY_TASK_REWARD);
|
||
});
|
||
});
|
||
|
||
describe('achievementy', () => {
|
||
test('odemykají se podle statistik', async () => {
|
||
await seed(USER, { caught: 600, coins: 0, goldenCaught: 1 });
|
||
const list = await getAchievements(USER);
|
||
const byId = Object.fromEntries(list.map(a => [a.id, a.unlocked]));
|
||
expect(byId['first-golden']).toBe(true);
|
||
expect(byId['catch-500']).toBe(true);
|
||
expect(byId['catch-2000']).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('anti-bot ban', () => {
|
||
test('po prahu selhání inspekce udělí ban', async () => {
|
||
const now = 6_000_000;
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, inspectionWindowStart: now });
|
||
let stats;
|
||
for (let i = 0; i < BAN_FAIL_THRESHOLD - 1; i++) {
|
||
stats = await reportInspectionFail(USER, now + i * 1000);
|
||
expect(stats.banUntil).toBe(0);
|
||
}
|
||
stats = await reportInspectionFail(USER, now + BAN_FAIL_THRESHOLD * 1000);
|
||
expect(stats.banUntil).toBe(now + BAN_FAIL_THRESHOLD * 1000 + BAN_DURATION_MS);
|
||
});
|
||
|
||
test('selhání mimo okno se nesčítají do banu', async () => {
|
||
const now = 7_000_000;
|
||
await seed(USER, { caught: 0, coins: 0, goldenCaught: 0, inspectionWindowStart: now });
|
||
await reportInspectionFail(USER, now);
|
||
// daleko za oknem → počítadlo se resetuje
|
||
const stats = await reportInspectionFail(USER, now + BAN_WINDOW_MS + 1000);
|
||
expect(stats.banUntil).toBe(0);
|
||
});
|
||
|
||
test('během banu se úlovky nezapočítávají', async () => {
|
||
const now = 8_000_000;
|
||
await seed(USER, { caught: 10, coins: 5, goldenCaught: 0, banUntil: now + BAN_DURATION_MS, lastCatchDay: formatDate(new Date(now)) });
|
||
const result = await recordCatches(USER, 20, 0, now);
|
||
expect(result.stats.caught).toBe(10); // beze změny
|
||
expect(result.coinsAwarded).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('getLeaderboard', () => {
|
||
test('řadí sestupně dle počtu chycených a respektuje limit', async () => {
|
||
await seed(USER, { caught: 100, coins: 0, goldenCaught: 2 });
|
||
await seed(OTHER, { caught: 200, coins: 0, goldenCaught: 0 });
|
||
await seed('jana', { caught: 50, coins: 0, goldenCaught: 10 });
|
||
|
||
const all = await getLeaderboard(10);
|
||
expect(all.map(e => e.login)).toEqual([OTHER, USER, 'jana']);
|
||
|
||
const top = await getLeaderboard(2);
|
||
expect(top).toHaveLength(2);
|
||
expect(top[0].login).toBe(OTHER);
|
||
});
|
||
});
|