Ctrl K
ComunidadGuías…

tests/test_trade_submission.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""One trade per form, however many times Save is pressed.

A test user pressed Save several times while a save waited on the price
provider, and every press recorded a trade. The form now carries an id minted
when it is rendered: the first POST claims it, and a repeat gets the first
one's answer instead of a second trade.
"""

from __future__ import annotations

import asyncio
import re

import pytest
from sqlalchemy import select

import factories as fac
from app import submissions
from app.models import Trade
from test_routes import bind_to_only_portfolio, make_login, reading, session_csrf

HTML = {"accept": "text/html"}


@pytest.fixture
def acme(client, session_factory):
    make_login(client, session_factory)
    with session_factory() as s:
        bind_to_only_portfolio(s)
        inst = fac.make_instrument(s, "ACME")
        fac.hold(s, inst)
        s.commit()
        return inst.id


def _id_on_the_form(client) -> str:
    page = client.get("/trade/new", headers=HTML)
    found = re.search(r'name="submission" value="([^"]+)"', page.text)
    assert found, "the trade form carries no submission id"
    return found.group(1)


def _save(client, session_factory, instrument_id, submission, **overrides):
    data = {"_csrf": session_csrf(session_factory), "instrument_id": str(instrument_id),
            "type": "buy", "trade_date": "2026-03-02", "quantity": "10",
            "unit_price": "5.00", "brokerage": "9.50", "submission": submission}
    data.update(overrides)
    return client.post("/trade/new", data=data, headers=HTML, follow_redirects=False)


def _trades(session_factory) -> int:
    with reading(session_factory) as s:
        return len(s.scalars(select(Trade)).all())


def test_each_rendering_of_the_form_gets_its_own_id(client, acme):
    first, second = _id_on_the_form(client), _id_on_the_form(client)
    assert first != second
    assert submissions.valid(first) and submissions.valid(second)


def test_pressing_save_twice_records_one_trade(client, session_factory, acme):
    submission = _id_on_the_form(client)

    first = _save(client, session_factory, acme, submission)
    second = _save(client, session_factory, acme, submission)

    assert first.status_code == second.status_code == 303
    assert second.headers["location"] == first.headers["location"] == "/holding/ACME"
    assert _trades(session_factory) == 1


def test_a_refused_save_can_be_corrected_on_the_same_form(client, session_factory, acme):
    """The id is spent by a trade, not by an attempt: the refusal re-renders
    the form, and saving it once corrected must still work."""
    submission = _id_on_the_form(client)

    refused = _save(client, session_factory, acme, submission, quantity="0")
    corrected = _save(client, session_factory, acme, submission)

    assert refused.status_code == 200 and "greater than zero" in refused.text
    assert corrected.status_code == 303
    assert _trades(session_factory) == 1


def test_two_forms_record_two_trades(client, session_factory, acme):
    _save(client, session_factory, acme, _id_on_the_form(client))
    _save(client, session_factory, acme, _id_on_the_form(client))
    assert _trades(session_factory) == 2


@pytest.mark.parametrize("submission", ["", "x" * 5000, "not an id", "İ" * 40])
def test_an_id_that_is_not_one_is_ignored(client, session_factory, acme, submission):
    """Without a usable id the save is unguarded, as it was before; it must not
    fail, and nothing it sent is kept."""
    resp = _save(client, session_factory, acme, submission)
    assert resp.status_code == 303
    assert _trades(session_factory) == 1
    assert submission not in submissions._claims


def test_a_repeat_during_a_slow_save_waits_for_its_answer():
    """A double click sends the second POST while the first still waits on the
    provider. It must land where the first does, not record or guess."""
    async def scenario():
        token = submissions.mint()
        assert await submissions.claim(token) is None          # the first: go ahead
        repeat = asyncio.ensure_future(submissions.claim(token, wait=2.0))
        await asyncio.sleep(0.05)
        assert not repeat.done(), "the repeat should wait for the first"
        submissions.finish(token, "/holding/ACME")
        return await repeat
    assert asyncio.run(scenario()) == "/holding/ACME"


def test_a_repeat_after_a_failed_save_may_try_again():
    """A save that failed outright gives its id back."""
    async def scenario():
        token = submissions.mint()
        assert await submissions.claim(token) is None
        submissions.release(token)
        return await submissions.claim(token)
    assert asyncio.run(scenario()) is None