Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ jobs:

- name: Wait for health check
run: |
echo "Waiting for application to start at http://${{ steps.ec2_ip.outputs.ip }}/health ..."
echo "Waiting for application to start at http://${{ steps.ec2_ip.outputs.ip }}:8000/health ..."
for i in $(seq 1 20); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"http://${{ steps.ec2_ip.outputs.ip }}/health" || echo "000")
"http://${{ steps.ec2_ip.outputs.ip }}:8000/health" || echo "000")
if [ "$STATUS" = "200" ]; then
echo "Health check passed!"
exit 0
Expand Down
6 changes: 4 additions & 2 deletions apps/api/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from adapter_core.errors.base import AdapterError, ErrorResponse
from app.limiter import limiter
from app.routes import health, payments, email, webhooks
from app.routes import email, health, payments, webhooks

app = FastAPI(
title="OpenAdapter API",
Expand All @@ -20,7 +20,9 @@


@app.exception_handler(RateLimitExceeded)
async def openadapter_rate_limit_handler(_request: Request, _exc: RateLimitExceeded) -> JSONResponse:
async def openadapter_rate_limit_handler(
_request: Request, _exc: RateLimitExceeded
) -> JSONResponse:
return JSONResponse(
status_code=429,
content={
Expand Down
11 changes: 8 additions & 3 deletions apps/api/app/routes/email.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status

from adapter_core.errors.base import AdapterError, ErrorResponse
from adapter_core.models.email import EmailStatusResponse, SendEmailResponse
from adapter_core.models.email import SendEmailRequest
from adapter_core.models.email import (
EmailStatusResponse,
SendEmailRequest,
SendEmailResponse,
)
from app.config import Settings, get_settings
from app.limiter import api_rate_limit, limiter
from app.schemas.email import SendEmailRequestBody
Expand All @@ -26,7 +29,9 @@ def _adapter_error_to_http(exc: AdapterError) -> HTTPException:
)


@router.post("/send", response_model=SendEmailResponse, status_code=status.HTTP_202_ACCEPTED)
@router.post(
"/send", response_model=SendEmailResponse, status_code=status.HTTP_202_ACCEPTED
)
@limiter.limit(api_rate_limit)
async def send_email(
request: Request, # noqa: ARG001
Expand Down
4 changes: 3 additions & 1 deletion apps/api/app/routes/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ async def health() -> HealthResponse:
return HealthResponse(status="healthy", version="1.0.0", environment="production")


@router.get("/providers", response_model=ProvidersResponse, summary="List supported providers")
@router.get(
"/providers", response_model=ProvidersResponse, summary="List supported providers"
)
async def providers() -> ProvidersResponse:
return ProvidersResponse(**SUPPORTED_PROVIDERS)
10 changes: 8 additions & 2 deletions apps/api/app/routes/payments.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status

from adapter_core.errors.base import AdapterError, ErrorResponse
from adapter_core.models.payment import ChargeResponse, PaymentDetailsResponse, RefundResponse
from adapter_core.models.payment import (
ChargeResponse,
PaymentDetailsResponse,
RefundResponse,
)
from app.config import Settings, get_settings
from app.limiter import api_rate_limit, limiter
from app.schemas.payment import ChargeRequestBody, RefundRequestBody
Expand All @@ -25,7 +29,9 @@ def _adapter_error_to_http(exc: AdapterError) -> HTTPException:
)


@router.post("/charge", response_model=ChargeResponse, status_code=status.HTTP_201_CREATED)
@router.post(
"/charge", response_model=ChargeResponse, status_code=status.HTTP_201_CREATED
)
@limiter.limit(api_rate_limit)
async def charge(
request: Request, # noqa: ARG001
Expand Down
7 changes: 5 additions & 2 deletions apps/api/app/routes/webhooks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status

from adapter_core.errors.base import AdapterError, ErrorCode
from adapter_core.errors.base import ErrorCode
from adapter_core.models.webhook import NormalizedWebhookEvent
from adapter_core.utils.webhook import (
normalize_event_type,
verify_stripe_signature,
verify_hmac_sha256,
)
from app.config import Settings, get_settings

Expand Down Expand Up @@ -49,6 +48,7 @@ async def receive_payment_webhook(
payload = request.app.state # placeholder — parse body below
try:
import json

payload = json.loads(body)
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body")
Expand All @@ -65,6 +65,7 @@ async def receive_payment_webhook(
elif provider == "paypal":
try:
import json

payload = json.loads(body)
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body")
Expand All @@ -91,6 +92,7 @@ async def receive_email_webhook(
if provider == "sendgrid":
try:
import json

events = json.loads(body)
if not isinstance(events, list):
events = [events]
Expand All @@ -117,6 +119,7 @@ async def receive_email_webhook(
raise _verification_failed_exc(provider)
try:
import json

payload = json.loads(body)
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body")
Expand Down
7 changes: 5 additions & 2 deletions apps/api/app/schemas/email.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, ConfigDict, Field

from adapter_core.models.email import EmailStatusResponse, SendEmailResponse

Expand All @@ -8,7 +8,10 @@ class SendEmailRequestBody(BaseModel):

model_config = ConfigDict(populate_by_name=True)

provider: str | None = Field(default=None, description="Provider override; defaults to DEFAULT_EMAIL_PROVIDER")
provider: str | None = Field(
default=None,
description="Provider override; defaults to DEFAULT_EMAIL_PROVIDER",
)
to: list[str]
sender: str = Field(..., alias="from")
subject: str
Expand Down
13 changes: 10 additions & 3 deletions apps/api/app/schemas/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
class ChargeRequestBody(BaseModel):
"""API request body for POST /payments/charge."""

provider: str | None = Field(default=None, description="Provider override; defaults to DEFAULT_PAYMENT_PROVIDER")
amount: int = Field(..., description="Amount in smallest currency unit (e.g. cents)")
provider: str | None = Field(
default=None,
description="Provider override; defaults to DEFAULT_PAYMENT_PROVIDER",
)
amount: int = Field(
..., description="Amount in smallest currency unit (e.g. cents)"
)
currency: str = Field(default="USD")
customer_email: str
source_token: str
Expand All @@ -23,7 +28,9 @@ class RefundRequestBody(BaseModel):

provider: str | None = None
transaction_id: str
amount: int | None = Field(default=None, description="Partial refund amount; omit for full refund")
amount: int | None = Field(
default=None, description="Partial refund amount; omit for full refund"
)
reason: str | None = None


Expand Down
2 changes: 1 addition & 1 deletion apps/api/app/schemas/webhook.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from adapter_core.models.webhook import NormalizedWebhookEvent
from adapter_core.errors.base import ErrorResponse
from adapter_core.models.webhook import NormalizedWebhookEvent

__all__ = ["NormalizedWebhookEvent", "ErrorResponse"]
5 changes: 4 additions & 1 deletion apps/api/tests/integration/test_webhooks_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ def test_stripe_webhook_invalid_signature(client: TestClient) -> None:
response = client.post(
"/webhooks/payments/stripe",
content=body,
headers={"stripe-signature": "t=123,v1=badsig", "content-type": "application/json"},
headers={
"stripe-signature": "t=123,v1=badsig",
"content-type": "application/json",
},
)
assert response.status_code == 401

Expand Down
40 changes: 25 additions & 15 deletions apps/api/tests/unit/test_email_models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import pytest
from pydantic import ValidationError

from adapter_core.models.email import SendEmailRequest, SendEmailResponse, EmailStatusResponse
from adapter_core.models.email import (
EmailStatusResponse,
SendEmailRequest,
SendEmailResponse,
)


def test_send_email_request_via_alias() -> None:
req = SendEmailRequest.model_validate({
"provider": "sendgrid",
"to": ["user@example.com"],
"from": "team@openadapter.dev",
"subject": "Hello",
"text": "World",
})
req = SendEmailRequest.model_validate(
{
"provider": "sendgrid",
"to": ["user@example.com"],
"from": "team@openadapter.dev",
"subject": "Hello",
"text": "World",
}
)
assert req.sender == "team@openadapter.dev"
assert req.to == ["user@example.com"]

Expand All @@ -28,12 +34,14 @@ def test_send_email_request_via_field_name() -> None:

def test_send_email_request_missing_required() -> None:
with pytest.raises(ValidationError):
SendEmailRequest.model_validate({
"provider": "sendgrid",
"to": ["a@b.com"],
# missing "from" / sender
"subject": "Hi",
})
SendEmailRequest.model_validate(
{
"provider": "sendgrid",
"to": ["a@b.com"],
# missing "from" / sender
"subject": "Hi",
}
)


def test_send_email_response_defaults() -> None:
Expand All @@ -42,7 +50,9 @@ def test_send_email_response_defaults() -> None:


def test_email_status_response_optional_fields() -> None:
resp = EmailStatusResponse(provider="postmark", message_id="abc", status="delivered")
resp = EmailStatusResponse(
provider="postmark", message_id="abc", status="delivered"
)
assert resp.submitted_at is None


Expand Down
6 changes: 2 additions & 4 deletions apps/api/tests/unit/test_error_normalization.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import pytest

from adapter_core.errors.base import (
HTTP_STATUS_ERROR_MAP,
RETRYABLE_CODES,
AdapterError,
ErrorCode,
ErrorResponse,
HTTP_STATUS_ERROR_MAP,
RETRYABLE_CODES,
)


Expand Down
3 changes: 1 addition & 2 deletions apps/api/tests/unit/test_payment_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
from adapter_core.models.payment import (
ChargeRequest,
ChargeResponse,
RefundRequest,
RefundResponse,
PaymentDetailsResponse,
RefundRequest,
)


Expand Down
24 changes: 15 additions & 9 deletions apps/api/tests/unit/test_sendgrid_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import httpx
import pytest
import respx
import httpx

from adapter_core.errors.base import AdapterError, ErrorCode
from adapter_core.models.email import SendEmailRequest
Expand All @@ -12,13 +12,15 @@

@pytest.fixture
def email_payload() -> SendEmailRequest:
return SendEmailRequest.model_validate({
"provider": "sendgrid",
"to": ["user@example.com"],
"from": "team@openadapter.dev",
"subject": "Hello",
"text": "World",
})
return SendEmailRequest.model_validate(
{
"provider": "sendgrid",
"to": ["user@example.com"],
"from": "team@openadapter.dev",
"subject": "Hello",
"text": "World",
}
)


@respx.mock
Expand Down Expand Up @@ -71,7 +73,11 @@ async def test_get_message_success() -> None:
respx.get(f"{SG_BASE}/messages/msg_abc").mock(
return_value=httpx.Response(
200,
json={"msg_id": "msg_abc", "status": "delivered", "last_event_time": "2024-01-01T00:00:00Z"},
json={
"msg_id": "msg_abc",
"status": "delivered",
"last_event_time": "2024-01-01T00:00:00Z",
},
)
)
result = await ADAPTER.get_message("msg_abc")
Expand Down
2 changes: 1 addition & 1 deletion apps/api/tests/unit/test_stripe_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import httpx
import pytest
import respx
import httpx

from adapter_core.errors.base import AdapterError, ErrorCode
from adapter_core.models.payment import ChargeRequest, RefundRequest
Expand Down
Loading
Loading