mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 14:53:12 +00:00
* 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.
116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
"""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 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)
|