Skip to content
Open
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
45 changes: 42 additions & 3 deletions backend/api_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,48 @@ def request(
status_code = _first_available(operation.responses, (401, 403), default=400)
return MockResponse(status_code, {"code": 4002, "message": "Authentication required"})

if operation.request_body_required and not payload:
status_code = _first_available(operation.responses, (422, 400, 409), default=422)
return MockResponse(status_code, {"code": 4001, "message": "Invalid request parameters"})
if operation.request_body_required:
if payload is None:
status_code = _first_available(operation.responses, (422, 400, 409), default=422)
return MockResponse(status_code, {"code": 4001, "message": "Invalid request parameters"})

# Check for missing required fields
missing_fields = [field for field in operation.required_fields if field not in payload]
if missing_fields:
status_code = _first_available(operation.responses, (422, 400, 409), default=422)
return MockResponse(status_code, {"code": 4001, "message": f"Missing required parameters: {', '.join(missing_fields)}"})

# Check for malformed fields (incorrect types)
schema = _request_schema(self.spec, operation.path, operation.method)
properties = schema.get("properties", {}) if isinstance(schema, Mapping) else {}
for field, val in payload.items():
if field in properties:
field_schema = properties[field]
if isinstance(field_schema, Mapping):
expected_type = field_schema.get("type")
is_malformed = False
if expected_type == "boolean":
if not isinstance(val, bool):
is_malformed = True
elif expected_type == "integer":
if isinstance(val, bool) or not isinstance(val, int):
is_malformed = True
elif expected_type == "number":
if isinstance(val, bool) or not isinstance(val, (int, float)):
is_malformed = True
elif expected_type == "array":
if not isinstance(val, list):
is_malformed = True
elif expected_type == "object":
if not isinstance(val, dict):
is_malformed = True
elif expected_type == "string":
if not isinstance(val, str):
is_malformed = True

if is_malformed:
status_code = _first_available(operation.responses, (422, 400, 409), default=422)
return MockResponse(status_code, {"code": 4001, "message": f"Malformed field: {field}"})

status_code = min(operation.success_statuses or (200,))
return MockResponse(
Expand Down
2 changes: 1 addition & 1 deletion build.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def _normalize_arch(machine: str) -> Optional[str]:

def _normalize_os() -> Optional[str]:
system = platform.system().lower()
if system == "linux":
if system in {"linux", "android"}:
return "linux"
if system == "darwin":
return "macos"
Expand Down
87 changes: 87 additions & 0 deletions diagnostic/build-011bb3d4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"generated_at": "2026-06-19T13:48:27.749920+00:00",
"commit": "011bb3d4",
"diagnostic_logd": "diagnostic/build-011bb3d4.logd",
"diagnostic_logd_error": null,
"message_blocker": null,
"chunked": false,
"chunk_size_bytes": null,
"password": "Packed /data/data/com.termux/files/home/.cache/tent-of-trials/logd-workspace into /data/data/com.termux/files/home/jackjin-zeroeye/diagnostic/build-011bb3d4.logd",
"decrypt_command": "encryptly unpack diagnostic/build-011bb3d4.logd <outdir> --password Packed /data/data/com.termux/files/home/.cache/tent-of-trials/logd-workspace into /data/data/com.termux/files/home/jackjin-zeroeye/diagnostic/build-011bb3d4.logd",
"total_modules": 10,
"passed": 1,
"failed": 9,
"modules": [
{
"name": "backend",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'cargo'"
},
{
"name": "frontend",
"status": "FAIL",
"elapsed_seconds": 15.737,
"artifact": null,
"output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\nsh: 1: tsc: not found"
},
{
"name": "market",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'go'"
},
{
"name": "frailbox",
"status": "PASS",
"elapsed_seconds": 3.674,
"artifact": "/data/data/com.termux/files/home/jackjin-zeroeye/frailbox/frailbox",
"output": "gcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/logger.c -o build/src/logger.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/sandbox.c -o build/src/sandbox.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c main.c -o build/main.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude build/src/arena.o build/src/logger.o build/src/sandbox.o build/main.o -o frailbox -pie -z relro -z now\nsrc/arena.c:179:17: warning: comparison of distinct pointer types ('const void *' and 'char *') [-Wcompare-distinct-pointer-types]\n 179 | ptr < (char *)region->start + region->size) {\n | ~~~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n1 warning generated."
},
{
"name": "engine",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'cmake'"
},
{
"name": "compliance",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'javac'"
},
{
"name": "v2-market-stream",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'ruby'"
},
{
"name": "nfc-scanner",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'luac'"
},
{
"name": "openapi-haskell",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'ghc'"
},
{
"name": "openapi-tools",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'luac'"
}
],
"pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-011bb3d4.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging."
}
Binary file added diagnostic/build-011bb3d4.logd
Binary file not shown.
92 changes: 91 additions & 1 deletion tests/backend_api/test_openapi_contract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""Tests for the documented backend API contract."""
"""Tests for the documented backend API contract.

To run these tests locally, execute:
python3 -m pytest -v tests/backend_api
"""

from __future__ import annotations

Expand Down Expand Up @@ -102,3 +106,89 @@ def test_async_request_wrapper(api_client, auth_token):

assert response.status_code == 200
assert response.body["path"] == "/users"


def test_missing_required_fields_in_payload_returns_error(api_client, api_operations, valid_payloads, auth_token, api_spec):
body_operations = [op for op in api_operations if op.request_body_required and op.required_fields]

for operation in body_operations:
token = None if operation.path.startswith("/auth/") else auth_token
base_payload = valid_payloads.get((operation.method, operation.path))
if not base_payload:
continue

for missing_field in operation.required_fields:
# Create payload missing one required field
payload = {k: v for k, v in base_payload.items() if k != missing_field}
response = api_client.request(operation.method, operation.path, payload=payload, token=token)

assert response.status_code in {400, 409, 422}
assert response.body["code"] == 4001
assert "Missing required parameters" in response.body["message"]


def test_malformed_fields_in_payload_returns_error(api_client, api_operations, valid_payloads, auth_token, api_spec):
from backend.api_contract import _request_schema
body_operations = [op for op in api_operations if op.request_body_required]

for operation in body_operations:
token = None if operation.path.startswith("/auth/") else auth_token
base_payload = valid_payloads.get((operation.method, operation.path))
if not base_payload:
continue

schema = _request_schema(api_spec, operation.path, operation.method)
properties = schema.get("properties", {})
if not isinstance(properties, dict):
continue

for field, field_schema in properties.items():
if not isinstance(field_schema, dict):
continue
expected_type = field_schema.get("type")
if not expected_type or field not in base_payload:
continue

# Create a malformed value of a different type
if expected_type == "string":
malformed_value = 12345
elif expected_type in {"integer", "number"}:
malformed_value = "not-a-number"
elif expected_type == "boolean":
malformed_value = "not-a-boolean"
elif expected_type == "array":
malformed_value = "not-an-array"
elif expected_type == "object":
malformed_value = "not-an-object"
else:
continue

payload = dict(base_payload)
payload[field] = malformed_value

response = api_client.request(operation.method, operation.path, payload=payload, token=token)

assert response.status_code in {400, 409, 422}
assert response.body["code"] == 4001
assert "Malformed field" in response.body["message"]


def test_async_request_wrapper_negative_cases(api_client, api_operations, auth_token):
# Case 1: Missing auth token for protected endpoint via async wrapper
protected_operations = [op for op in api_operations if not op.path.startswith("/auth/")]
if protected_operations:
operation = protected_operations[0]
response = asyncio.run(api_client.request_async(operation.method, operation.path, token=None))
assert response.status_code in {401, 403, 400}
assert response.body["code"] == 4002
assert "Authentication required" in response.body["message"]

# Case 2: Missing required payload for body-requiring endpoint via async wrapper
body_operations = [op for op in api_operations if op.request_body_required]
if body_operations:
operation = body_operations[0]
token = None if operation.path.startswith("/auth/") else auth_token
response = asyncio.run(api_client.request_async(operation.method, operation.path, payload=None, token=token))
assert response.status_code in {400, 409, 422}
assert response.body["code"] == 4001
assert "Invalid request parameters" in response.body["message"]
Binary file modified tools/encryptly/linux-arm64/encryptly
Binary file not shown.