mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-24 03:58:05 +09:00
171 lines
5.4 KiB
Python
171 lines
5.4 KiB
Python
"""Event-subscription use-case and HTTP-boundary tests."""
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
from flask_jwt_extended import create_access_token
|
|
from werkzeug.security import generate_password_hash
|
|
|
|
|
|
def _developer_headers(db, username):
|
|
from app.models import User
|
|
|
|
user = User(
|
|
email=f'{username}@test.local',
|
|
username=username,
|
|
password_hash=generate_password_hash('testpass'),
|
|
role=User.ROLE_DEVELOPER,
|
|
is_active=True,
|
|
)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user, {'Authorization': f'Bearer {create_access_token(identity=user.id)}'}
|
|
|
|
|
|
def _create(client, headers, name='Deploy hook'):
|
|
return client.post('/api/v1/event-subscriptions/', headers=headers, json={
|
|
'name': name,
|
|
'url': 'https://hooks.example.test/serverkit',
|
|
'events': ['app.deployed'],
|
|
'generate_secret': True,
|
|
})
|
|
|
|
|
|
def test_subscription_crud_uses_typed_boundary(client, auth_headers):
|
|
created_response = _create(client, auth_headers)
|
|
assert created_response.status_code == 201
|
|
created = created_response.get_json()
|
|
assert created['name'] == 'Deploy hook'
|
|
assert created['secret'].startswith('whsec_')
|
|
|
|
listed = client.get(
|
|
'/api/v1/event-subscriptions/', headers=auth_headers
|
|
).get_json()['subscriptions']
|
|
assert [item['id'] for item in listed] == [created['id']]
|
|
assert 'secret' not in listed[0]
|
|
|
|
updated_response = client.put(
|
|
f"/api/v1/event-subscriptions/{created['id']}",
|
|
headers=auth_headers,
|
|
json={'name': 'Release hook', 'retry_count': 5},
|
|
)
|
|
assert updated_response.status_code == 200
|
|
assert updated_response.get_json()['name'] == 'Release hook'
|
|
assert updated_response.get_json()['retry_count'] == 5
|
|
|
|
deleted = client.delete(
|
|
f"/api/v1/event-subscriptions/{created['id']}", headers=auth_headers
|
|
)
|
|
assert deleted.status_code == 200
|
|
|
|
missing = client.get(
|
|
f"/api/v1/event-subscriptions/{created['id']}", headers=auth_headers
|
|
)
|
|
assert missing.status_code == 404
|
|
assert missing.get_json()['code'] == 'event_subscription_not_found'
|
|
|
|
|
|
def test_subscription_schema_rejects_unknown_and_invalid_fields(
|
|
client, auth_headers,
|
|
):
|
|
response = client.post(
|
|
'/api/v1/event-subscriptions/',
|
|
headers={**auth_headers, 'X-Request-ID': 'subscription-contract'},
|
|
json={
|
|
'name': 'Hook',
|
|
'url': 'https://hooks.example.test/serverkit',
|
|
'events': [],
|
|
'surprise': True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
payload = response.get_json()
|
|
assert payload['code'] == 'invalid_body'
|
|
assert payload['request_id'] == 'subscription-contract'
|
|
assert set(payload['details']['fields']) == {'events', 'surprise'}
|
|
|
|
|
|
def test_non_owner_cannot_read_or_mutate_subscription(client, app):
|
|
from app import db
|
|
|
|
owner, owner_headers = _developer_headers(db, 'hook_owner')
|
|
_other, other_headers = _developer_headers(db, 'hook_other')
|
|
created = _create(client, owner_headers, name='Private hook').get_json()
|
|
|
|
for method, path, kwargs in (
|
|
('get', f"/api/v1/event-subscriptions/{created['id']}", {}),
|
|
('put', f"/api/v1/event-subscriptions/{created['id']}", {
|
|
'json': {'name': 'Stolen'},
|
|
}),
|
|
('delete', f"/api/v1/event-subscriptions/{created['id']}", {}),
|
|
):
|
|
response = getattr(client, method)(path, headers=other_headers, **kwargs)
|
|
assert response.status_code == 403
|
|
assert response.get_json()['code'] == 'permission_denied'
|
|
|
|
listed = client.get(
|
|
'/api/v1/event-subscriptions/', headers=other_headers
|
|
).get_json()['subscriptions']
|
|
assert listed == []
|
|
assert owner.id == created['user_id']
|
|
|
|
|
|
def test_retry_commits_pending_state_before_enqueue(
|
|
client, auth_headers, monkeypatch,
|
|
):
|
|
from app import db
|
|
from app.models.event_subscription import EventDelivery
|
|
from app.services import event_service
|
|
|
|
subscription = _create(client, auth_headers).get_json()
|
|
delivery = EventDelivery(
|
|
subscription_id=subscription['id'],
|
|
event_type='app.deployed',
|
|
status=EventDelivery.STATUS_FAILED,
|
|
next_retry_at=None,
|
|
)
|
|
db.session.add(delivery)
|
|
db.session.commit()
|
|
|
|
observed = []
|
|
|
|
def capture(delivery_id):
|
|
row = db.session.get(EventDelivery, delivery_id)
|
|
observed.append((row.id, row.status))
|
|
|
|
monkeypatch.setattr(event_service, 'enqueue_webhook_delivery', capture)
|
|
response = client.post(
|
|
f"/api/v1/event-subscriptions/{subscription['id']}"
|
|
f'/deliveries/{delivery.id}/retry',
|
|
headers=auth_headers,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.get_json()['status'] == EventDelivery.STATUS_PENDING
|
|
assert observed == [(delivery.id, EventDelivery.STATUS_PENDING)]
|
|
|
|
|
|
def test_event_subscription_routes_do_not_own_persistence():
|
|
path = (
|
|
Path(__file__).resolve().parents[1]
|
|
/ 'app' / 'api' / 'event_subscriptions.py'
|
|
)
|
|
tree = ast.parse(path.read_text(encoding='utf-8'))
|
|
|
|
imported_names = {
|
|
alias.name
|
|
for node in ast.walk(tree)
|
|
if isinstance(node, ast.ImportFrom)
|
|
for alias in node.names
|
|
}
|
|
session_calls = [
|
|
node
|
|
for node in ast.walk(tree)
|
|
if isinstance(node, ast.Attribute) and node.attr == 'session'
|
|
]
|
|
|
|
assert 'EventSubscription' not in imported_names
|
|
assert 'EventDelivery' not in imported_names
|
|
assert session_calls == []
|