Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/api/routes/pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,23 @@ async def add_operated(
principal: Principal = Depends(require_perm("edit_settings")),
) -> APIResponse[PairsOut]:
store = OperatedPairStore()
before = store.symbols()
sym = await _validate_addable(body.symbol, client)
# `before` é o conjunto EFETIVO (DB > env), não a tabela crua: é o que o loop
# realmente opera e o que a auditoria deve registrar.
before = operated_pairs()
# M2: idempotência. Checar contra o efetivo cobre o duplicado na tabela E o par
# já operado via fallback do env (tabela vazia) — nos dois casos é 409, não 201.
if sym in before:
raise HTTPException(status_code=409, detail={
"error": "already_operated",
"message": f"'{sym}' já está no conjunto operado."})
# M3: na PRIMEIRA adição (tabela crua vazia — `store.symbols()`, não
# `operated_pairs()`, que nunca é vazio por causa do default), semeia o conjunto
# efetivo do env ANTES de adicionar o novo par. Sem isso, inserir o 1º par
# tornaria a tabela não-vazia e os pares do env (DB > env) cairiam em silêncio.
if not store.symbols():
for s in before:
store.add(s)
store.add(sym)
after = store.symbols()
ledger.log_decision("config_changed", {
Expand Down
48 changes: 47 additions & 1 deletion tests/api/test_operated_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,53 @@ def test_add_validates_and_persists(env):
r = c.post("/v1/pairs/operated", json={"symbol": "ETH/USDT"})
assert r.status_code == 201, r.text
assert "ETH/USDT" in [o["symbol"] for o in r.json()["data"]["operados"]]
assert OperatedPairStore().symbols() == ["ETH/USDT"]
# M3: a 1ª adição semeia o conjunto efetivo do env (SYMBOLS=BTC/USDT) antes de
# inserir o novo par — o BTC não some ao materializar a tabela.
assert set(OperatedPairStore().symbols()) == {"BTC/USDT", "ETH/USDT"}


def test_add_is_idempotent_409(env):
# M2: adicionar um par já operado retorna 409 (não 201), sem linha duplicada.
OperatedPairStore().add("ETH/USDT") # tabela já não-vazia → sem semeadura
c = TestClient(create_app())
r = c.post("/v1/pairs/operated", json={"symbol": "ETH/USDT"})
assert r.status_code == 409, r.text
assert r.json()["error"] == "already_operated" # handler passa o detail como body
assert OperatedPairStore().symbols() == ["ETH/USDT"] # sem duplicata


def test_add_already_effective_via_env_is_409(env):
# M2: um par já operado via fallback do env (tabela vazia) também é 409 — e NÃO
# dispara semeadura (a request de conflito não altera estado).
assert OperatedPairStore().symbols() == []
c = TestClient(create_app())
r = c.post("/v1/pairs/operated", json={"symbol": "BTC/USDT"}) # BTC vem do env
assert r.status_code == 409, r.text
assert OperatedPairStore().symbols() == [] # nada semeado/adicionado


def test_first_add_seeds_env_pairs(env):
# M3: reproduz e corrige o incidente do BTC — tabela vazia + env=BTC, adicionar
# ETH pela primeira vez preserva o BTC (não cai em silêncio na transição env→DB).
assert OperatedPairStore().symbols() == []
assert operated_pairs() == ["BTC/USDT"] # env fallback
c = TestClient(create_app())
r = c.post("/v1/pairs/operated", json={"symbol": "ETH/USDT"})
assert r.status_code == 201, r.text
assert set(OperatedPairStore().symbols()) == {"BTC/USDT", "ETH/USDT"}
assert set(operated_pairs()) == {"BTC/USDT", "ETH/USDT"}


def test_first_add_audit_diff_is_incremental(env):
# M3: a auditoria registra a migração honesta ["BTC/USDT"]→["BTC","ETH"], não
# ["]→["ETH"] (que escondia o BTC sumindo).
ledger = env
c = TestClient(create_app())
c.post("/v1/pairs/operated", json={"symbol": "ETH/USDT"})
evt = next(e for e in ledger.get_events("config_changed")
if e["data"].get("scope") == "pairs")
assert evt["data"]["before"]["operated"] == ["BTC/USDT"]
assert set(evt["data"]["after"]["operated"]) == {"BTC/USDT", "ETH/USDT"}


def test_add_rejects_non_usdt_and_non_allowlisted(env):
Expand Down
Loading