265 lines
9.1 KiB
Python
265 lines
9.1 KiB
Python
"""Flask API integration tests for authentication and route boundaries."""
|
|
|
|
import uuid
|
|
|
|
SERVICE_KEY = "integration-service-key-0123456789abcdef"
|
|
ALLOWED_DISCORD_ID = "integration-discord-user"
|
|
|
|
|
|
def _bearer(token):
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def test_health_and_error_routes(client):
|
|
assert client.get("/health/live").get_json() == {"status": "ok"}
|
|
assert client.get("/health/ready").get_json() == {"status": "ready"}
|
|
assert client.get("/health").get_json() == {"status": "ready"}
|
|
|
|
missing = client.get("/api/route-that-does-not-exist")
|
|
assert missing.status_code == 404
|
|
assert missing.get_json() == {"error": "not found"}
|
|
|
|
wrongMethod = client.get("/api/register")
|
|
assert wrongMethod.status_code == 405
|
|
assert wrongMethod.get_json() == {"error": "method not allowed"}
|
|
|
|
invalidJson = client.post(
|
|
"/api/register", data="[]", content_type="application/json"
|
|
)
|
|
assert invalidJson.status_code == 400
|
|
assert invalidJson.get_json() == {"error": "JSON object required"}
|
|
|
|
|
|
def test_password_registration_keeps_values_parameterized_and_protected(client):
|
|
from core import postgres
|
|
|
|
injectedUsername = "alice'; DROP TABLE users; --"
|
|
injectedUUID = str(uuid.uuid4())
|
|
response = client.post(
|
|
"/api/register",
|
|
json={
|
|
"username": injectedUsername,
|
|
"password": "long-enough-password",
|
|
"timezone": "America/Chicago",
|
|
"id": injectedUUID,
|
|
"password_hashed": "attacker-controlled",
|
|
"created_at": "1900-01-01T00:00:00Z",
|
|
"unexpected_admin": True,
|
|
},
|
|
)
|
|
assert response.status_code == 201, response.get_json()
|
|
assert postgres.table_exists("users")
|
|
|
|
record = postgres.select_one("users", {"username": injectedUsername})
|
|
assert record is not None
|
|
assert str(record["id"]) != injectedUUID
|
|
assert bytes(record["password_hashed"]) != b"attacker-controlled"
|
|
assert record["timezone"] == "America/Chicago"
|
|
assert record["created_at"].year > 1900
|
|
|
|
login = client.post(
|
|
"/api/login",
|
|
json={"username": injectedUsername, "password": "long-enough-password"},
|
|
)
|
|
assert login.status_code == 200
|
|
assert login.get_json()["token"]
|
|
|
|
|
|
def test_jwt_user_key_creation_authentication_and_revocation(
|
|
client, createAuthenticatedUser
|
|
):
|
|
from core import postgres
|
|
|
|
user = createAuthenticatedUser("api-key-owner")
|
|
jwtHeaders = _bearer(user["token"])
|
|
|
|
profile = client.get(f"/api/user/{user['user_uuid']}", headers=jwtHeaders)
|
|
assert profile.status_code == 200
|
|
assert profile.get_json()["username"] == user["username"]
|
|
assert "password_hashed" not in profile.get_json()
|
|
|
|
update = client.put(
|
|
f"/api/user/{user['user_uuid']}",
|
|
headers=jwtHeaders,
|
|
json={"timezone": "America/Chicago", "username": "cannot-change"},
|
|
)
|
|
assert update.status_code == 200
|
|
assert client.get(
|
|
"/api/user/me/timezone", headers=jwtHeaders
|
|
).get_json() == {"timezone": "America/Chicago"}
|
|
|
|
created = client.post(
|
|
"/api/keys", headers=jwtHeaders, json={"name": "integration key"}
|
|
)
|
|
assert created.status_code == 201, created.get_json()
|
|
keyRecord = created.get_json()
|
|
secret = keyRecord["key"]
|
|
assert secret.startswith("llmbot_user_")
|
|
assert "key_hash" not in keyRecord
|
|
storedKey = postgres.select_one("api_keys", {"id": keyRecord["id"]})
|
|
assert storedKey["key_hash"] != secret
|
|
assert secret not in {str(value) for value in storedKey.values()}
|
|
|
|
listed = client.get("/api/keys", headers=jwtHeaders)
|
|
assert listed.status_code == 200
|
|
listedKey = listed.get_json()["keys"][0]
|
|
assert listedKey["id"] == keyRecord["id"]
|
|
assert "key" not in listedKey
|
|
assert "key_hash" not in listedKey
|
|
|
|
apiKeyHeaders = _bearer(secret)
|
|
timezone = client.get("/api/user/me/timezone", headers=apiKeyHeaders)
|
|
assert timezone.status_code == 200
|
|
assert timezone.get_json() == {"timezone": "America/Chicago"}
|
|
assert postgres.select_one("api_keys", {"id": keyRecord["id"]})[
|
|
"last_used_at"
|
|
] is not None
|
|
|
|
cannotManageKeys = client.post(
|
|
"/api/keys", headers=apiKeyHeaders, json={"name": "nested key"}
|
|
)
|
|
assert cannotManageKeys.status_code == 401
|
|
|
|
revoked = client.delete(f"/api/keys/{keyRecord['id']}", headers=jwtHeaders)
|
|
assert revoked.status_code == 200
|
|
assert postgres.select_one("api_keys", {"id": keyRecord["id"]})[
|
|
"revoked_at"
|
|
] is not None
|
|
assert client.get("/api/user/me/timezone", headers=apiKeyHeaders).status_code == 401
|
|
|
|
|
|
def test_discord_allowlist_and_service_key_boundaries(client, createAuthenticatedUser):
|
|
serviceHeaders = _bearer(SERVICE_KEY)
|
|
|
|
assert client.post(
|
|
"/api/auth/discord/session",
|
|
json={"discord_id": ALLOWED_DISCORD_ID},
|
|
).status_code == 401
|
|
assert client.post(
|
|
"/api/auth/discord/session",
|
|
headers=_bearer("not-the-configured-service-key-0123456789"),
|
|
json={"discord_id": ALLOWED_DISCORD_ID},
|
|
).status_code == 401
|
|
|
|
denied = client.post(
|
|
"/api/auth/discord/session",
|
|
headers=serviceHeaders,
|
|
json={"discord_id": "not-allowlisted", "display_name": "No Access"},
|
|
)
|
|
assert denied.status_code == 403
|
|
|
|
accepted = client.post(
|
|
"/api/auth/discord/session",
|
|
headers=serviceHeaders,
|
|
json={"discord_id": ALLOWED_DISCORD_ID, "display_name": "First Name"},
|
|
)
|
|
assert accepted.status_code == 200, accepted.get_json()
|
|
discordSession = accepted.get_json()
|
|
assert discordSession["timezone"] == "UTC"
|
|
assert discordSession["token"]
|
|
|
|
repeated = client.post(
|
|
"/api/auth/discord/session",
|
|
headers=serviceHeaders,
|
|
json={"discord_id": ALLOWED_DISCORD_ID, "display_name": "New Name"},
|
|
)
|
|
assert repeated.status_code == 200
|
|
assert repeated.get_json()["user_uuid"] == discordSession["user_uuid"]
|
|
|
|
profile = client.get(
|
|
f"/api/user/{discordSession['user_uuid']}",
|
|
headers=_bearer(discordSession["token"]),
|
|
)
|
|
assert profile.status_code == 200
|
|
assert profile.get_json()["username"] is None
|
|
|
|
passwordUser = createAuthenticatedUser("service-boundary-user")
|
|
assert client.post(
|
|
"/api/auth/discord/session",
|
|
headers=_bearer(passwordUser["token"]),
|
|
json={"discord_id": ALLOWED_DISCORD_ID},
|
|
).status_code == 401
|
|
assert client.get(
|
|
"/api/user/me/timezone", headers=serviceHeaders
|
|
).status_code == 401
|
|
|
|
|
|
def test_service_outbox_claim_retry_and_delivery_routes(client):
|
|
from core import outbox, postgres
|
|
|
|
serviceHeaders = _bearer(SERVICE_KEY)
|
|
session = client.post(
|
|
"/api/auth/discord/session",
|
|
headers=serviceHeaders,
|
|
json={"discord_id": ALLOWED_DISCORD_ID, "display_name": "Recipient"},
|
|
).get_json()
|
|
queued = outbox.enqueue_message(
|
|
session["user_uuid"],
|
|
"discord_dm",
|
|
{"content": "integration delivery"},
|
|
idempotency_key=f"api-outbox-{uuid.uuid4()}",
|
|
)
|
|
|
|
claimed = client.post(
|
|
"/api/internal/outbox/claim",
|
|
headers=serviceHeaders,
|
|
json={"worker_id": "api-worker-one", "limit": 1},
|
|
)
|
|
assert claimed.status_code == 200
|
|
message = claimed.get_json()["messages"][0]
|
|
assert message["id"] == str(queued["id"])
|
|
assert message["provider_user_id"] == ALLOWED_DISCORD_ID
|
|
assert message["content"] == "integration delivery"
|
|
assert message["worker_id"] == "api-worker-one"
|
|
|
|
missingWorker = client.post(
|
|
f"/api/internal/outbox/{message['id']}/result",
|
|
headers=serviceHeaders,
|
|
json={"status": "sent"},
|
|
)
|
|
assert missingWorker.status_code == 409
|
|
|
|
wrongWorker = client.post(
|
|
f"/api/internal/outbox/{message['id']}/result",
|
|
headers=serviceHeaders,
|
|
json={"status": "sent", "worker_id": "not-the-lease-owner"},
|
|
)
|
|
assert wrongWorker.status_code == 409
|
|
|
|
stillLeased = outbox.get_message(message["id"])
|
|
assert stillLeased["status"] == "delivering"
|
|
assert stillLeased["leased_by"] == "api-worker-one"
|
|
assert stillLeased["delivered_at"] is None
|
|
|
|
retried = client.post(
|
|
f"/api/internal/outbox/{message['id']}/result",
|
|
headers=serviceHeaders,
|
|
json={
|
|
"status": "retry",
|
|
"error": "temporary failure",
|
|
"worker_id": "api-worker-one",
|
|
},
|
|
)
|
|
assert retried.status_code == 200
|
|
assert retried.get_json()["message"]["status"] == "pending"
|
|
|
|
postgres.execute(
|
|
"UPDATE outbound_messages SET available_at = NOW() WHERE id = %(id)s",
|
|
{"id": message["id"]},
|
|
)
|
|
claimedAgain = client.post(
|
|
"/api/internal/outbox/claim",
|
|
headers=serviceHeaders,
|
|
json={"worker_id": "api-worker-two", "limit": 1},
|
|
)
|
|
assert claimedAgain.status_code == 200
|
|
assert claimedAgain.get_json()["messages"][0]["attempts"] == 2
|
|
|
|
delivered = client.post(
|
|
f"/api/internal/outbox/{message['id']}/result",
|
|
headers=serviceHeaders,
|
|
json={"status": "sent", "worker_id": "api-worker-two"},
|
|
)
|
|
assert delivered.status_code == 200
|
|
assert delivered.get_json()["message"]["status"] == "delivered"
|