fix(pl24): kendi bütçe frenimiz "hesap banlandı" alarmı üretmesin #271
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PL24AuthService } from "./pl24-auth.service";
|
||||
import { PL24BudgetExceededError } from "./pl24-budget.service";
|
||||
|
||||
// Oturum modeli (1 login → PL24TOKEN çerezi → yalnız authorize ile yenileme),
|
||||
// PL24_TR_DISABLED köprüsü ve login devre kesici. Ağ yok: global fetch stub'ı;
|
||||
@@ -459,3 +460,33 @@ describe("PL24AuthService — 1 saatlik kesinti alarmı (Telegram)", () => {
|
||||
expect(redis.store.has("pl24:auth:fail-since:de")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Kendi günlük tavanımız dolduğunda bu bir GİRİŞ HATASI değildir.
|
||||
*
|
||||
* Prod 2026-09-21: tavan dolunca `attemptLogin` içindeki `budget.consume()`
|
||||
* fırlattı, dış `catch` bunu `{ error }` düzleştirdi, `loginWithLock` giriş
|
||||
* hatası sanıp devre kesiciyi tetikledi ve kesinti sayacını başlattı — bir saat
|
||||
* sonra Telegram "hesap banlanmış olabilir" dedi. Hesap sağlamdı (tarayıcıdan
|
||||
* giriş çalışıyordu, aynı gün 143 başarılı auth ve 0 × 401).
|
||||
*/
|
||||
describe("bütçe reddi ban alarmı üretmemeli", () => {
|
||||
it("bütçe hatası olduğu gibi yukarı çıkar, giriş hatasına çevrilmez", async () => {
|
||||
const { svc, redis, telegram, budget } = makeService();
|
||||
budget.consume = async () => {
|
||||
throw new PL24BudgetExceededError("user", 1205, 1200);
|
||||
};
|
||||
const fetchSpy = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
|
||||
await expect((svc as never as Exposed).login("de")).rejects.toBeInstanceOf(
|
||||
PL24BudgetExceededError,
|
||||
);
|
||||
|
||||
// Kesinti sayacı başlamamalı → Telegram susmalı.
|
||||
expect(telegram.sent).toHaveLength(0);
|
||||
const failKeys = Object.keys(redis.store ?? {}).filter((k) => k.includes("fail-since"));
|
||||
expect(failKeys).toHaveLength(0);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ import { Injectable, Logger, type OnModuleInit, UnauthorizedException } from "@n
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { TelegramService } from "../../common/telegram.service";
|
||||
import { RedisService } from "../../redis/redis.service";
|
||||
import { PL24BudgetService } from "./pl24-budget.service";
|
||||
import { PL24BudgetExceededError, PL24BudgetService } from "./pl24-budget.service";
|
||||
import { PL24_DEFAULTS, PL24_ENDPOINTS, PL24_USER_AGENT } from "./pl24.constants";
|
||||
import {
|
||||
PL24AuthorizeRequest,
|
||||
@@ -485,6 +485,15 @@ export class PL24AuthService implements OnModuleInit {
|
||||
return { sessionToken };
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
// OUR OWN daily cap refusing to spend is not a login failure. Flattening it
|
||||
// into `{ error }` made `loginWithLock` treat it as one: it tripped the
|
||||
// login circuit breaker and started the outage timer, so an hour later the
|
||||
// Telegram alert claimed the account might be banned — while the account
|
||||
// was fine (browser login worked, telemetry showed 0 × 401 and 143
|
||||
// successful auth calls that same day). Observed on prod 2026-09-21:
|
||||
// "PL24 daily HTTP budget exhausted (user lane: 1205/1200)". Let it through
|
||||
// as itself so the caller can skip PL24 without anyone being paged.
|
||||
if (error instanceof PL24BudgetExceededError) throw error;
|
||||
if (err.name === "TimeoutError") return { error: "giris zaman asimina ugradi" };
|
||||
return { error: err.message };
|
||||
}
|
||||
@@ -524,6 +533,9 @@ export class PL24AuthService implements OnModuleInit {
|
||||
return token;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
// Same reasoning as attemptLogin: a self-imposed budget stop is not an
|
||||
// authorization problem and must not be reported as one.
|
||||
if (error instanceof PL24BudgetExceededError) throw error;
|
||||
this.logger.error(`Service authorization error (${account}): ${err.message}`);
|
||||
throw err instanceof UnauthorizedException
|
||||
? err
|
||||
|
||||
@@ -139,3 +139,28 @@ describe("PL24BudgetService — telemetri", () => {
|
||||
expect(telemetry.events[0].statusCode).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Bütçe reddi bir giriş hatası DEĞİLDİR (plv2.md, bulgu auth-16).
|
||||
*
|
||||
* Prod 2026-09-21: günlük tavan dolunca `attemptLogin` içindeki
|
||||
* `budget.consume()` fırlattı, dış `catch` bunu `{ error }` düzleştirdi,
|
||||
* `loginWithLock` bunu giriş hatası sanıp devre kesiciyi tetikledi ve kesinti
|
||||
* sayacını başlattı → bir saat sonra Telegram "hesap banlanmış olabilir" dedi.
|
||||
* Oysa hesap sağlamdı: tarayıcıdan giriş çalışıyordu, aynı gün 143 başarılı
|
||||
* auth çağrısı ve 0 × 401 vardı.
|
||||
*/
|
||||
describe("PL24BudgetExceededError — kendi frenimiz, upstream hatası değil", () => {
|
||||
it("kendi hata sınıfını taşır, düz Error değil", () => {
|
||||
const err = new PL24BudgetExceededError("user", 1205, 1200);
|
||||
expect(err).toBeInstanceOf(PL24BudgetExceededError);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it("mesajı hangi şeridin ve hangi sayının durduğunu söyler", () => {
|
||||
const err = new PL24BudgetExceededError("user", 1205, 1200);
|
||||
expect(err.message).toContain("1205");
|
||||
expect(err.message).toContain("1200");
|
||||
expect(err.message.toLowerCase()).toContain("user");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,14 @@ export class PL24BudgetService {
|
||||
const key = this.dayKey();
|
||||
spent = await this.redis.incr(key);
|
||||
if (spent === 1) await this.redis.expire(key, 8 * 86_400);
|
||||
// Per-lane counters. The shared total tells us WHEN the cap was hit but not
|
||||
// WHO spent it, and sizing the cap needs that split: on 2026-09-21 the
|
||||
// total hit 1200 and every user decode after 16:10 UTC was refused, with
|
||||
// no way to tell from `proxy_logs` how much of it was warm-up prefetch
|
||||
// versus somebody waiting on a screen.
|
||||
const laneKey = `${key}:${backfill ? "worker" : "user"}`;
|
||||
const laneSpent = await this.redis.incr(laneKey);
|
||||
if (laneSpent === 1) await this.redis.expire(laneKey, 8 * 86_400);
|
||||
} catch {
|
||||
return; // Redis down → never block PL24 on telemetry
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user