test_cli.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """Unit tests for the ``backend.app.cli`` kiosk-bootstrap subcommand."""
  2. from __future__ import annotations
  3. from collections.abc import AsyncGenerator
  4. import pytest
  5. import pytest_asyncio
  6. from sqlalchemy import select
  7. from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
  8. from backend.app.cli import DEFAULT_KIOSK_KEY_NAME, KioskBootstrapError, kiosk_bootstrap
  9. from backend.app.core.auth import _validate_api_key
  10. from backend.app.core.database import Base
  11. from backend.app.models.api_key import APIKey
  12. @pytest_asyncio.fixture
  13. async def session_maker() -> AsyncGenerator[async_sessionmaker, None]:
  14. engine = create_async_engine("sqlite+aiosqlite:///:memory:")
  15. async with engine.begin() as conn:
  16. await conn.run_sync(Base.metadata.create_all)
  17. maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
  18. try:
  19. yield maker
  20. finally:
  21. await engine.dispose()
  22. @pytest.mark.asyncio
  23. @pytest.mark.unit
  24. async def test_bootstrap_creates_key_when_none_exists(session_maker):
  25. key = await kiosk_bootstrap(
  26. DEFAULT_KIOSK_KEY_NAME,
  27. force=False,
  28. session_maker=session_maker,
  29. ensure_schema=False,
  30. )
  31. assert key.startswith("bb_")
  32. assert len(key) > 20
  33. async with session_maker() as db:
  34. rows = (await db.execute(select(APIKey))).scalars().all()
  35. assert len(rows) == 1
  36. row = rows[0]
  37. assert row.name == DEFAULT_KIOSK_KEY_NAME
  38. assert row.enabled is True
  39. assert row.can_queue is False
  40. assert row.can_control_printer is False
  41. assert row.can_read_status is True
  42. assert row.printer_ids is None
  43. assert row.expires_at is None
  44. assert row.key_prefix.startswith("bb_")
  45. assert row.key_hash != key # stored value is a hash, not the plaintext
  46. @pytest.mark.asyncio
  47. @pytest.mark.unit
  48. async def test_bootstrap_refuses_to_overwrite_without_force(session_maker):
  49. first = await kiosk_bootstrap(
  50. DEFAULT_KIOSK_KEY_NAME,
  51. force=False,
  52. session_maker=session_maker,
  53. ensure_schema=False,
  54. )
  55. with pytest.raises(KioskBootstrapError) as exc_info:
  56. await kiosk_bootstrap(
  57. DEFAULT_KIOSK_KEY_NAME,
  58. force=False,
  59. session_maker=session_maker,
  60. ensure_schema=False,
  61. )
  62. assert "already exists" in str(exc_info.value)
  63. assert "--force" in str(exc_info.value)
  64. # First key survives unchanged and still validates
  65. async with session_maker() as db:
  66. row = (await db.execute(select(APIKey))).scalar_one()
  67. validated = await _validate_api_key(db, first)
  68. assert validated is not None
  69. assert validated.id == row.id
  70. @pytest.mark.asyncio
  71. @pytest.mark.unit
  72. async def test_bootstrap_force_rotates_existing_key(session_maker):
  73. first = await kiosk_bootstrap(
  74. DEFAULT_KIOSK_KEY_NAME,
  75. force=False,
  76. session_maker=session_maker,
  77. ensure_schema=False,
  78. )
  79. second = await kiosk_bootstrap(
  80. DEFAULT_KIOSK_KEY_NAME,
  81. force=True,
  82. session_maker=session_maker,
  83. ensure_schema=False,
  84. )
  85. assert first != second
  86. async with session_maker() as db:
  87. rows = (await db.execute(select(APIKey))).scalars().all()
  88. assert len(rows) == 1 # old row was deleted, not duplicated
  89. # Old key no longer validates, new key does
  90. assert await _validate_api_key(db, first) is None
  91. validated = await _validate_api_key(db, second)
  92. assert validated is not None
  93. assert validated.name == DEFAULT_KIOSK_KEY_NAME
  94. @pytest.mark.asyncio
  95. @pytest.mark.unit
  96. async def test_bootstrap_custom_name(session_maker):
  97. key = await kiosk_bootstrap(
  98. "custom-kiosk-name",
  99. force=False,
  100. session_maker=session_maker,
  101. ensure_schema=False,
  102. )
  103. async with session_maker() as db:
  104. row = (await db.execute(select(APIKey))).scalar_one()
  105. assert row.name == "custom-kiosk-name"
  106. validated = await _validate_api_key(db, key)
  107. assert validated is not None
  108. assert validated.name == "custom-kiosk-name"