svcforge: reference implementation
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
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%.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user