afterhours-shift-manager/tests/slack_bot/conftest.py
Adam Moussa 3a26343cb7
Add pytest suite and wire it into CI (#85) (#86)
* Add pytest suite and wire it into CI

Stands up the first automated tests for the repo (151 tests) and turns on
the CI test step.

- Lift slack-bot handlers out of create_app() closures to module level so
  they're unit-testable; create_app is now a thin Bolt-wiring layer. No
  behavior change (handler entrypoints and create_app signature unchanged).
- tests/ mirrors src/: shared layer (schedule, blocks, 3CX client,
  ring_scheduler, secrets) + all four Lambdas (pay math, drop/swap/pick/
  admin/register/rate, pickup button, roster sync, queue scheduler).
- All boundaries mocked: DynamoDB/SES/Secrets via moto, 3CX HTTP via
  responses, Slack via fakes, time via freezegun. No real network/AWS.
- pyproject.toml pytest config (pythonpath=src/shared, importlib mode);
  per-package conftest loads each app.py under a unique name to avoid the
  four-app.py collision. tests/requirements.txt for test-only deps.
- ci.yaml: run-tests: true (reusable workflow auto-installs deps) and lint
  the tests dir too.
- README Testing section.

Closes #85

* Add least-privilege permissions block to CI workflow

Resolves the CodeQL actions/missing-workflow-permissions alert: the CI
workflow now restricts GITHUB_TOKEN to contents: read (it only checks out,
lints, and runs tests).

* Stop logging extension numbers in 3CX queue updates

Resolves 3 high CodeQL py/clear-text-logging-sensitive-data alerts: the
queue/ring-group forwarding logs no longer include the routed extension
values (closed/holiday/extension). Non-sensitive context (resource id,
queue number) is retained.
2026-06-01 19:07:08 -04:00

53 lines
1.2 KiB
Python

"""Load src/slack-bot/app.py under a unique module name so it doesn't collide
with the other Lambdas' app.py modules in sys.modules, plus common test doubles."""
import importlib.util
import pathlib
import sys
from unittest.mock import MagicMock
import pytest
_ROOT = pathlib.Path(__file__).resolve().parents[2]
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, _ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
@pytest.fixture
def slackbot_app():
return _load("slackbot_app", "src/slack-bot/app.py")
@pytest.fixture
def respond():
return MagicMock(name="respond")
@pytest.fixture
def client():
return MagicMock(name="client")
@pytest.fixture
def routing_spy(slackbot_app, monkeypatch):
"""Replace the 3CX routing call with a spy so tests can assert on it
without touching the network."""
spy = MagicMock(name="_update_3cx_routing")
monkeypatch.setattr(slackbot_app, "_update_3cx_routing", spy)
return spy
@pytest.fixture
def text_of():
"""Helper: pull the `text=` kwarg from a mock's most recent call."""
def _get(mock):
return mock.call_args.kwargs.get("text", "")
return _get