Skip to content

Commit da1fa80

Browse files
author
Neo
committed
[P3] Make licensing WIRE handlers case-tolerant for the client's snake_case
The iOS client encodes every body to snake_case, so register_ip / license_ip must accept ip_id / evidence_hash exactly as they accept ipId / evidenceHash. Add a _first(body, *keys) helper and read owner/type/name/ipId/evidenceHash via both cases; honest 400 when a required field is truly absent. This lets the P3 client execution legs (LicensingView.registerIP / issueLicense) actually traverse to ip_royalties through the _call seam. Regenerated ROUTES.md (handler line numbers) + snake_case test. Full suite 656 green.
1 parent dca7523 commit da1fa80

3 files changed

Lines changed: 63 additions & 15 deletions

File tree

docs/ROUTES.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
| POST | `/api/v1/governance/snapshot/vote` | `_handle_snapshot_vote` | service_routes.py:350 | |
6464
| POST | `/api/v1/governance/treasury/transfer` | `_handle_treasury_transfer` | service_routes.py:351 | |
6565
| POST | `/api/v1/governance/vote` | `_handle_governance_vote` | service_routes.py:227 | |
66-
| POST | `/api/v1/groups` | `_handle_groups_create` | service_routes.py:1881 | |
66+
| POST | `/api/v1/groups` | `_handle_groups_create` | service_routes.py:1908 | |
6767
| POST | `/api/v1/iap/asn` | `handle_iap_asn` | server.py:2056 ||
6868
| POST | `/api/v1/iap/verify` | `handle_iap_verify` | server.py:2055 ||
6969
| POST | `/api/v1/identity/create` | `_handle_did_create` | service_routes.py:201 | |
@@ -83,14 +83,14 @@
8383
| POST | `/api/v1/legal/agreement/execute` | `_handle_agreement_execute` | service_routes.py:365 | |
8484
| POST | `/api/v1/legal/dispute/file` | `_handle_legal_dispute_file` | service_routes.py:366 | |
8585
| POST | `/api/v1/legal/license/grant` | `_handle_license_grant` | service_routes.py:364 | |
86-
| POST | `/api/v1/licensing/ip` | `_handle_licensing_register_ip` | service_routes.py:1885 | |
87-
| POST | `/api/v1/licensing/licenses` | `_handle_licensing_create_license` | service_routes.py:1887 | |
86+
| POST | `/api/v1/licensing/ip` | `_handle_licensing_register_ip` | service_routes.py:1912 | |
87+
| POST | `/api/v1/licensing/licenses` | `_handle_licensing_create_license` | service_routes.py:1914 | |
8888
| POST | `/api/v1/loyalty/earn` | `_handle_loyalty_earn` | service_routes.py:246 | |
8989
| POST | `/api/v1/loyalty/redeem` | `_handle_loyalty_redeem` | service_routes.py:247 | |
9090
| POST | `/api/v1/marketplace/buy` | `_handle_marketplace_buy` | service_routes.py:223 | |
9191
| POST | `/api/v1/marketplace/list` | `_handle_marketplace_list` | service_routes.py:222 | |
92-
| GET | `/api/v1/messaging/conversations` | `_handle_messaging_conversations` | service_routes.py:1871 | |
93-
| GET | `/api/v1/messaging/conversations/{conversationId}/messages` | `_handle_messaging_messages` | service_routes.py:1872 | |
92+
| GET | `/api/v1/messaging/conversations` | `_handle_messaging_conversations` | service_routes.py:1898 | |
93+
| GET | `/api/v1/messaging/conversations/{conversationId}/messages` | `_handle_messaging_messages` | service_routes.py:1899 | |
9494
| POST | `/api/v1/nft/batch-mint` | `_handle_nft_batch_mint` | service_routes.py:303 | |
9595
| POST | `/api/v1/nft/bridge` | `_handle_nft_bridge` | service_routes.py:305 | |
9696
| POST | `/api/v1/nft/collection/create` | `_handle_nft_collection_create` | service_routes.py:195 | |

gateway/service_routes.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,29 +1823,56 @@ async def _handle_groups_create(self, request: web.Request) -> web.Response:
18231823
)
18241824
return self._ok(result)
18251825

1826+
@staticmethod
1827+
def _first(body: dict, *keys: str):
1828+
"""First present, non-empty value among *keys*.
1829+
1830+
The iOS client encodes every body to snake_case, so a client-supplied
1831+
camelCase key (``ipId``) arrives as ``ip_id``. Accept both forms.
1832+
"""
1833+
for k in keys:
1834+
v = body.get(k)
1835+
if v not in (None, ""):
1836+
return v
1837+
return None
1838+
18261839
async def _handle_licensing_register_ip(self, request: web.Request) -> web.Response:
18271840
body = await self._parse_body(request)
1828-
self._require(body, "owner", "type", "name")
1841+
owner = self._first(body, "owner")
1842+
ip_type = self._first(body, "type", "ip_type")
1843+
name = self._first(body, "name")
1844+
if not owner or not ip_type or not name:
1845+
raise web.HTTPBadRequest(
1846+
text=json.dumps({"error": "owner, type and name are required"}),
1847+
content_type="application/json",
1848+
)
18291849
result = await self._call(
18301850
"ip_royalties", "register_ip",
1831-
owner=body["owner"],
1832-
ip_type=body["type"],
1851+
owner=owner,
1852+
ip_type=ip_type,
18331853
metadata={
1834-
"title": body["name"],
1835-
"description": body.get("description", ""),
1836-
"content_hash": body.get("evidenceHash", ""),
1854+
"title": name,
1855+
"description": self._first(body, "description") or "",
1856+
"content_hash": self._first(body, "evidenceHash", "evidence_hash") or "",
18371857
},
18381858
)
18391859
return self._ok(result)
18401860

18411861
async def _handle_licensing_create_license(self, request: web.Request) -> web.Response:
18421862
body = await self._parse_body(request)
1843-
self._require(body, "ipId", "recipient", "terms")
1863+
ip_id = self._first(body, "ipId", "ip_id")
1864+
recipient = self._first(body, "recipient")
1865+
terms = self._first(body, "terms")
1866+
if not ip_id or not recipient or terms is None:
1867+
raise web.HTTPBadRequest(
1868+
text=json.dumps({"error": "ipId, recipient and terms are required"}),
1869+
content_type="application/json",
1870+
)
18441871
result = await self._call(
18451872
"ip_royalties", "license_ip",
1846-
ip_id=body["ipId"],
1847-
licensee=body["recipient"],
1848-
terms=body["terms"],
1873+
ip_id=ip_id,
1874+
licensee=recipient,
1875+
terms=terms,
18491876
)
18501877
return self._ok(result)
18511878

tests/test_p2_route_completion.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ async def test_licensing_create_license_never_500(client):
7272
assert resp.status != 500, await resp.text()
7373

7474

75+
async def test_licensing_accepts_snake_case_body(client):
76+
# The iOS client encodes bodies to snake_case; the handlers must accept it
77+
# (ip_id / evidence_hash) exactly as they accept camelCase. A snake_case
78+
# register must NOT be rejected as "missing name/type" (would be a 400).
79+
reg = await client.post(
80+
"/api/v1/licensing/ip",
81+
json={"owner": "0xabc", "type": "patent", "name": "Snakey", "evidence_hash": "0xfeed"},
82+
)
83+
assert reg.status in (200, 400), await reg.text() # 200 record or honest ip_type 400
84+
assert reg.status != 500
85+
86+
lic = await client.post(
87+
"/api/v1/licensing/licenses",
88+
json={"ip_id": "ip_x", "recipient": "0xabc", "terms": {"license_type": "non_exclusive"}},
89+
)
90+
# Reached the service with ip_id populated -> not the "ipId required" 400.
91+
assert lic.status != 500, await lic.text()
92+
if lic.status == 400:
93+
assert "required" not in (await lic.json()).get("error", "").lower()
94+
95+
7596
# ── NOT_IMPLEMENTED routes: honest 501, never a fake 200 ───────────────
7697

7798
NOT_IMPLEMENTED = [

0 commit comments

Comments
 (0)