50c2fe2a1e
ci / lint (push) Successful in 1m19s
ci / unit (push) Failing after 1m2s
ci / integration (push) Has been skipped
ci / types (push) Successful in 1m37s
ci / security (push) Failing after 38s
ci / dockerfile (push) Successful in 14s
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped
Complete working build of the system learn-python/ teaches. 164 tests, mypy --strict clean, domain coverage 99%.
105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
"""InstanceRepo against real SQL."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from svcforge_core.domain.models import TaskKind
|
|
from svcforge_core.domain.states import InstanceState
|
|
from svcforge_core.repo.db import DictPool
|
|
from svcforge_core.repo.instances import InstanceRepo
|
|
from svcforge_core.repo.tasks import TaskRepo
|
|
from tests.integration.helpers import build_instance
|
|
|
|
|
|
async def test_create_then_get_round_trips(pool: DictPool) -> None:
|
|
repo = InstanceRepo(pool)
|
|
inst = build_instance()
|
|
async with pool.connection() as conn:
|
|
created = await repo.create(conn, inst)
|
|
assert created.id == inst.id
|
|
assert created.release_name == inst.release_name
|
|
|
|
got = await repo.get(inst.id, team="platform")
|
|
assert got is not None
|
|
assert got.service_type == "elasticsearch"
|
|
assert got.state is InstanceState.REQUESTED
|
|
# timestamptz round-trips as aware, or every later comparison raises TypeError.
|
|
assert got.created_at.tzinfo is not None
|
|
|
|
|
|
async def test_get_by_other_team_is_none_not_403(pool: DictPool) -> None:
|
|
"""A wrong-team id is indistinguishable from a missing one."""
|
|
repo = InstanceRepo(pool)
|
|
inst = build_instance(team="platform")
|
|
async with pool.connection() as conn:
|
|
await repo.create(conn, inst)
|
|
|
|
assert await repo.get(inst.id, team="quant") is None
|
|
|
|
|
|
async def test_list_is_filtered_by_team(pool: DictPool) -> None:
|
|
repo = InstanceRepo(pool)
|
|
async with pool.connection() as conn:
|
|
await repo.create(conn, build_instance(team="platform"))
|
|
await repo.create(conn, build_instance(team="quant"))
|
|
|
|
mine = await repo.list(team="platform")
|
|
assert len(mine) == 1
|
|
assert all(i.team == "platform" for i in mine)
|
|
|
|
|
|
async def test_update_state_cas_rejects_stale_expectation(pool: DictPool) -> None:
|
|
repo = InstanceRepo(pool)
|
|
inst = build_instance()
|
|
async with pool.connection() as conn:
|
|
await repo.create(conn, inst)
|
|
|
|
ok = await repo.update_state(inst.id, InstanceState.REQUESTED, InstanceState.PROVISIONING)
|
|
assert ok is True
|
|
|
|
# The row already moved: the second caller must lose, and must not raise.
|
|
lost = await repo.update_state(inst.id, InstanceState.REQUESTED, InstanceState.PROVISIONING)
|
|
assert lost is False
|
|
|
|
|
|
async def test_update_state_sets_endpoint(pool: DictPool) -> None:
|
|
repo = InstanceRepo(pool)
|
|
inst = build_instance()
|
|
async with pool.connection() as conn:
|
|
await repo.create(conn, inst)
|
|
await repo.update_state(inst.id, InstanceState.REQUESTED, InstanceState.PROVISIONING)
|
|
ok = await repo.update_state(
|
|
inst.id, InstanceState.PROVISIONING, InstanceState.READY, endpoint="http://es:9200"
|
|
)
|
|
assert ok is True
|
|
got = await repo.get(inst.id, team="platform")
|
|
assert got is not None
|
|
assert got.endpoint == "http://es:9200"
|
|
assert got.state is InstanceState.READY
|
|
|
|
|
|
@pytest.mark.parametrize("explode", [True])
|
|
async def test_instance_and_task_roll_back_together(pool: DictPool, explode: bool) -> None:
|
|
"""The reason the queue is in Postgres, as an executable claim.
|
|
|
|
If the transaction aborts, BOTH the instance and its provision task must vanish.
|
|
An instance with no task never gets built; a task with no instance is an orphan.
|
|
"""
|
|
instances, tasks = InstanceRepo(pool), TaskRepo(pool)
|
|
inst = build_instance()
|
|
|
|
with pytest.raises(RuntimeError):
|
|
async with pool.connection() as conn, conn.transaction():
|
|
await instances.create(conn, inst)
|
|
await tasks.enqueue(conn, inst.id, TaskKind.PROVISION)
|
|
if explode:
|
|
raise RuntimeError("boom, mid-transaction")
|
|
|
|
assert await instances.get(inst.id, team="platform") is None
|
|
async with pool.connection() as conn, conn.cursor() as cur:
|
|
await cur.execute("select count(*) as n from tasks")
|
|
row = await cur.fetchone()
|
|
assert row is not None
|
|
assert row["n"] == 0
|