afterhours-shift-manager/tests/conftest.py

136 lines
4.3 KiB
Python
Raw Normal View History

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
"""Shared pytest fixtures: AWS env, a moto-backed DynamoDB table, a ShiftSchedule
bound to it, and a seeding helper for the single-table data model."""
import boto3
import pytest
from moto import mock_aws
TABLE_NAME = "afterhours-shifts"
@pytest.fixture(autouse=True)
def aws_env(monkeypatch):
"""Fake AWS credentials + the env vars the handlers read.
Autouse so no test can accidentally reach real AWS. 3CX env vars are left
unset by default, so the slack-bot's _update_3cx_routing short-circuits
unless a test sets them explicitly.
"""
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("AWS_SECURITY_TOKEN", "testing")
monkeypatch.setenv("AWS_SESSION_TOKEN", "testing")
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("SHIFT_TABLE", TABLE_NAME)
monkeypatch.setenv("SHIFT_CHANNEL", "C_TEST")
monkeypatch.delenv("QUEUE_NUMBER", raising=False)
monkeypatch.delenv("TCX_SECRET_PREFIX", raising=False)
def _create_table(dynamodb):
table = dynamodb.create_table(
TableName=TABLE_NAME,
KeySchema=[
{"AttributeName": "PK", "KeyType": "HASH"},
{"AttributeName": "SK", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "PK", "AttributeType": "S"},
{"AttributeName": "SK", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
)
table.wait_until_exists()
return table
@pytest.fixture
def dynamodb_table(aws_env):
"""A mocked `afterhours-shifts` table (PK/SK, PAY_PER_REQUEST). The moto
context stays open for the duration of the test."""
with mock_aws():
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
yield _create_table(dynamodb)
@pytest.fixture
def schedule(dynamodb_table):
"""A ShiftSchedule bound to the mocked table."""
from shared.schedule import ShiftSchedule
return ShiftSchedule()
class Seeder:
"""Thin helper to put single-table items matching schedule.py's key model."""
def __init__(self, table):
self.table = table
def roster(self, ext, name, slack_user_id="", **extra):
self.table.put_item(
Item={
"PK": "ROSTER",
"SK": ext,
"name": name,
"extension": ext,
"slack_user_id": slack_user_id,
**extra,
}
)
def weekly(self, day_name, ext, name, shift_type="night"):
sk = f"{day_name}-Day" if shift_type == "day" else day_name
self.table.put_item(
Item={"PK": "WEEKLY", "SK": sk, "extension": ext, "name": name}
)
def override(self, date_str, ext, name, shift_type="night"):
sk = f"{date_str}-DAY" if shift_type == "day" else date_str
self.table.put_item(
Item={"PK": "OVERRIDE", "SK": sk, "extension": ext, "name": name}
)
def open_shift(self, date_str, shift_type="night"):
sk = f"{date_str}-DAY" if shift_type == "day" else date_str
self.table.put_item(
Item={"PK": "OVERRIDE", "SK": sk, "extension": "OPEN", "name": "Open"}
)
def config(self, **kwargs):
self.table.put_item(Item={"PK": "CONFIG", "SK": "CONFIG", **kwargs})
def holiday(self, date_str, slots=2, label="Holiday", assignees=None, **extra):
from decimal import Decimal
self.table.put_item(
Item={
"PK": "HOLIDAY",
"SK": date_str,
"slots": slots,
"assignees": assignees or {},
"multiplier": Decimal("1.5"),
"label": label,
"created_at": "2026-06-01T00:00:00-04:00",
"created_by": "",
"activated": False,
"schedule_names": [],
**extra,
}
)
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
def schedule_post(self, channel, message_ts, week_start="2026-06-01"):
self.table.put_item(
Item={
"PK": "SCHEDULE_POST",
"SK": channel,
"message_ts": message_ts,
"week_start": week_start,
}
)
@pytest.fixture
def seed(dynamodb_table):
return Seeder(dynamodb_table)