Skip to content

Commit 117cc56

Browse files
MonolithicMonkYuki I
and
Yuki I
authored
fix: align ledger config schema with API response (openwallet-foundation#3615)
* fix: align ledger config schema with API response - Replace ledger_config_list with production/non_production_ledgers - Add missing fields to LedgerConfigInstanceSchema - Update OpenAPI documentation Resolves openwallet-foundation#123 Signed-off-by: Yuki I <omoge.real@gmail.com> * test: add coverage for ledger config schemas - Validate schema structure and required fields - Test UUID generation in LedgerConfigInstanceSchema - Verify production/non-production ledger grouping Signed-off-by: Yuki I <omoge.real@gmail.com> * fix: final schema and test adjustments Signed-off-by: Yuki I <omoge.real@gmail.com> --------- Signed-off-by: Yuki I <omoge.real@gmail.com> Co-authored-by: Yuki I <omoge.real@gmail.com>
1 parent cdfe8b4 commit 117cc56

File tree

9 files changed

+200
-32
lines changed

9 files changed

+200
-32
lines changed

acapy_agent/ledger/multiple_ledger/ledger_config_schema.py

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,25 @@ def __init__(
2121
self,
2222
*,
2323
id: Optional[str] = None,
24-
is_production: str = True,
25-
genesis_transactions: Optional[str] = None,
26-
genesis_file: Optional[str] = None,
27-
genesis_url: Optional[str] = None,
24+
is_production: bool = True,
25+
is_write: bool = False,
26+
keepalive: int = 5,
27+
read_only: bool = False,
28+
socks_proxy: Optional[str] = None,
29+
pool_name: Optional[str] = None,
30+
endorser_alias: Optional[str] = None,
31+
endorser_did: Optional[str] = None,
2832
):
2933
"""Initialize LedgerConfigInstance."""
30-
self.id = id
34+
self.id = id or str(uuid4())
3135
self.is_production = is_production
32-
self.genesis_transactions = genesis_transactions
33-
self.genesis_file = genesis_file
34-
self.genesis_url = genesis_url
36+
self.is_write = is_write
37+
self.keepalive = keepalive
38+
self.read_only = read_only
39+
self.socks_proxy = socks_proxy
40+
self.pool_name = pool_name or self.id
41+
self.endorser_alias = endorser_alias
42+
self.endorser_did = endorser_did
3543

3644

3745
class LedgerConfigInstanceSchema(BaseModelSchema):
@@ -43,13 +51,46 @@ class Meta:
4351
model_class = LedgerConfigInstance
4452
unknown = EXCLUDE
4553

46-
id = fields.Str(required=False, metadata={"description": "ledger_id"})
47-
is_production = fields.Bool(required=False, metadata={"description": "is_production"})
48-
genesis_transactions = fields.Str(
49-
required=False, metadata={"description": "genesis_transactions"}
54+
id = fields.Str(
55+
required=True,
56+
metadata={
57+
"description": "Ledger identifier. Auto-generated UUID4 if not provided",
58+
"example": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
59+
},
60+
)
61+
is_production = fields.Bool(
62+
required=True, metadata={"description": "Production-grade ledger (true/false)"}
63+
)
64+
is_write = fields.Bool(
65+
required=False,
66+
metadata={"description": "Write capability enabled (default: False)"},
67+
)
68+
keepalive = fields.Int(
69+
required=False,
70+
metadata={
71+
"description": "Keep-alive timeout in seconds for idle connections",
72+
"default": 5,
73+
},
74+
)
75+
read_only = fields.Bool(
76+
required=False, metadata={"description": "Read-only access (default: False)"}
77+
)
78+
socks_proxy = fields.Str(
79+
required=False, metadata={"description": "SOCKS proxy URL (optional)"}
80+
)
81+
pool_name = fields.Str(
82+
required=False,
83+
metadata={
84+
"description": "Ledger pool name (defaults to ledger ID if not specified)",
85+
"example": "bcovrin-test-pool",
86+
},
87+
)
88+
endorser_alias = fields.Str(
89+
required=False, metadata={"description": "Endorser service alias (optional)"}
90+
)
91+
endorser_did = fields.Str(
92+
required=False, metadata={"description": "Endorser DID (optional)"}
5093
)
51-
genesis_file = fields.Str(required=False, metadata={"description": "genesis_file"})
52-
genesis_url = fields.Str(required=False, metadata={"description": "genesis_url"})
5394

5495
@pre_load
5596
def validate_id(self, data, **kwargs):
@@ -58,12 +99,27 @@ def validate_id(self, data, **kwargs):
5899
data["id"] = str(uuid4())
59100
return data
60101

102+
@pre_load
103+
def set_defaults(self, data, **kwargs):
104+
"""Set default values for optional fields."""
105+
data.setdefault("is_write", False)
106+
data.setdefault("keepalive", 5)
107+
data.setdefault("read_only", False)
108+
return data
109+
61110

62111
class LedgerConfigListSchema(OpenAPISchema):
63112
"""Schema for Ledger Config List."""
64113

65-
ledger_config_list = fields.List(
66-
fields.Nested(LedgerConfigInstanceSchema(), required=True), required=True
114+
production_ledgers = fields.List( # Changed from ledger_config_list
115+
fields.Nested(LedgerConfigInstanceSchema(), required=True),
116+
required=True,
117+
metadata={"description": "Production ledgers (may be empty)"},
118+
)
119+
non_production_ledgers = fields.List( # Added new field
120+
fields.Nested(LedgerConfigInstanceSchema(), required=True),
121+
required=True,
122+
metadata={"description": "Non-production ledgers (may be empty)"},
67123
)
68124

69125

acapy_agent/ledger/tests/test_routes.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import json
2+
import pytest
3+
import uuid
4+
from marshmallow import ValidationError
25
from typing import Optional
36
from unittest import IsolatedAsyncioTestCase
7+
from uuid_utils import uuid4
48

59
from ...connections.models.conn_record import ConnRecord
610
from ...ledger.base import BaseLedger
@@ -9,7 +13,11 @@
913
from ...ledger.multiple_ledger.ledger_requests_executor import (
1014
IndyLedgerRequestsExecutor,
1115
)
12-
from ...ledger.multiple_ledger.ledger_config_schema import ConfigurableWriteLedgersSchema
16+
from ...ledger.multiple_ledger.ledger_config_schema import (
17+
ConfigurableWriteLedgersSchema,
18+
LedgerConfigInstanceSchema,
19+
LedgerConfigListSchema,
20+
)
1321

1422
from ...multitenant.base import BaseMultitenantManager
1523
from ...multitenant.manager import MultitenantManager
@@ -869,6 +877,110 @@ async def test_get_ledger_config_x(self):
869877
with self.assertRaises(test_module.web.HTTPForbidden):
870878
await test_module.get_ledger_config(self.request)
871879

880+
async def test_get_ledger_config_structure(self):
881+
"""Test the structure of the ledger config response."""
882+
mock_manager = mock.MagicMock(BaseMultipleLedgerManager, autospec=True)
883+
mock_manager.get_prod_ledgers = mock.CoroutineMock(return_value={"test_1": None})
884+
mock_manager.get_nonprod_ledgers = mock.CoroutineMock(
885+
return_value={"test_2": None}
886+
)
887+
self.profile.context.injector.bind_instance(
888+
BaseMultipleLedgerManager, mock_manager
889+
)
890+
891+
self.context.settings["ledger.ledger_config_list"] = [
892+
{
893+
"id": "test_1",
894+
"is_production": True,
895+
"is_write": True,
896+
"keepalive": 5,
897+
"read_only": False,
898+
"pool_name": "test_pool",
899+
"socks_proxy": None,
900+
},
901+
{
902+
"id": "test_2",
903+
"is_production": False,
904+
"is_write": False,
905+
"keepalive": 10,
906+
"read_only": True,
907+
"pool_name": "non_prod_pool",
908+
"socks_proxy": None,
909+
},
910+
]
911+
912+
with mock.patch.object(
913+
test_module.web, "json_response", mock.Mock()
914+
) as json_response:
915+
await test_module.get_ledger_config(self.request)
916+
917+
response_data = json_response.call_args[0][0]
918+
assert "production_ledgers" in response_data
919+
assert "non_production_ledgers" in response_data
920+
921+
prod_ledger = response_data["production_ledgers"][0]
922+
assert prod_ledger == {
923+
"id": "test_1",
924+
"is_production": True,
925+
"is_write": True,
926+
"keepalive": 5,
927+
"read_only": False,
928+
"pool_name": "test_pool",
929+
"socks_proxy": None,
930+
}
931+
932+
non_prod_ledger = response_data["non_production_ledgers"][0]
933+
assert non_prod_ledger == {
934+
"id": "test_2",
935+
"is_production": False,
936+
"is_write": False,
937+
"keepalive": 10,
938+
"read_only": True,
939+
"pool_name": "non_prod_pool",
940+
"socks_proxy": None,
941+
}
942+
943+
async def test_ledger_config_schema_validation(self):
944+
"""Test schema validation for required fields."""
945+
schema = LedgerConfigInstanceSchema()
946+
947+
minimal_data = {
948+
"is_production": True,
949+
"is_write": False,
950+
"keepalive": 5,
951+
"read_only": False,
952+
}
953+
loaded = schema.load(minimal_data)
954+
assert loaded.pool_name == loaded.id
955+
assert loaded.is_write is False
956+
957+
with pytest.raises(ValidationError) as exc:
958+
schema.load({"is_production": "not_bool"})
959+
assert "is_production" in exc.value.messages
960+
961+
async def test_ledger_config_id_generation(self):
962+
"""Test automatic ID generation when missing."""
963+
schema = LedgerConfigInstanceSchema()
964+
965+
data = {
966+
"is_production": True,
967+
"is_write": False, # Add required fields
968+
"keepalive": 5,
969+
"read_only": False,
970+
}
971+
loaded = schema.load(data)
972+
assert uuid.UUID(loaded.id, version=4)
973+
974+
explicit_id = str(uuid4())
975+
loaded = schema.load({"id": explicit_id, "is_production": True})
976+
assert loaded.id == explicit_id
977+
978+
async def test_empty_ledger_lists(self):
979+
schema = LedgerConfigListSchema()
980+
empty_data = {"production_ledgers": [], "non_production_ledgers": []}
981+
loaded = schema.load(empty_data)
982+
assert loaded == empty_data
983+
872984
# Multiple Ledgers Configured
873985
async def test_get_write_ledgers_multiple(self):
874986
# Mock the multiple ledger manager

conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def stub_ursa_bbs_signatures() -> Stub:
147147
def pytest_sessionstart(session):
148148
global STUBS, POSTGRES_URL, ENABLE_PTVSD
149149
args = sys.argv
150-
150+
151151
# copied from __main__.py:init_debug
152152
ENABLE_PTVSD = os.getenv("ENABLE_PTVSD", "").lower()
153153
ENABLE_PTVSD = ENABLE_PTVSD and ENABLE_PTVSD not in ("false", "0")

demo/runners/acme.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
check_requires,
1515
log_msg,
1616
log_status,
17-
log_timer,
1817
prompt,
1918
prompt_loop,
2019
)

demo/runners/alice.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ async def main(args):
158158
log_status("#9 Input faber.py invitation details")
159159
await input_invitation(alice_agent)
160160

161-
options = " (3) Send Message\n" " (4) Input New Invitation\n"
161+
options = " (3) Send Message\n (4) Input New Invitation\n"
162162
if alice_agent.endorser_role and alice_agent.endorser_role == "author":
163163
options += " (D) Set Endorser's DID\n"
164164
if alice_agent.multitenant:

demo/runners/performance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def done_send(fut: asyncio.Task):
457457
def test_cred(index: int) -> dict:
458458
return {
459459
"name": "Alice Smith",
460-
"date": f"{2020+index}-05-28",
460+
"date": f"{2020 + index}-05-28",
461461
"degree": "Maths",
462462
"age": "24",
463463
}
@@ -579,7 +579,7 @@ async def check_received_pings(agent, issue_count, pb):
579579
avg = recv_timer.duration / issue_count
580580
item_short = "ping" if action == "ping" else "cred"
581581
item_long = "ping exchange" if action == "ping" else "credential"
582-
faber.log(f"Average time per {item_long}: {avg:.2f}s ({1/avg:.2f}/s)")
582+
faber.log(f"Average time per {item_long}: {avg:.2f}s ({1 / avg:.2f}/s)")
583583

584584
if alice.postgres:
585585
await alice.collect_postgres_stats(f"{issue_count} {item_short}s")

demo/runners/support/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1783,7 +1783,7 @@ async def connect_wallet_to_endorser(agent, endorser_agent):
17831783
# setup endorser meta-data on our connection
17841784
log_msg("Setup author agent meta-data ...")
17851785
await agent.admin_POST(
1786-
f"/transactions/{agent.endorser_connection_id }/set-endorser-role",
1786+
f"/transactions/{agent.endorser_connection_id}/set-endorser-role",
17871787
params={"transaction_my_job": "TRANSACTION_AUTHOR"},
17881788
)
17891789
endorser_did = endorser_agent.endorser_public_did

scenarios/examples/json_ld/example.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ async def main():
278278
{
279279
"path": ["$.issuer"],
280280
"purpose": "The claim must be from one of the specified issuers", # noqa: E501
281-
"filter": {"const": p256_alice_did}
281+
"filter": {"const": p256_alice_did},
282282
},
283283
{
284284
"path": ["$.credentialSubject.givenName"],
@@ -289,9 +289,7 @@ async def main():
289289
}
290290
],
291291
"id": str(uuid4()),
292-
"format": {
293-
"ldp_vp": {"proof_type": ["EcdsaSecp256r1Signature2019"]}
294-
},
292+
"format": {"ldp_vp": {"proof_type": ["EcdsaSecp256r1Signature2019"]}},
295293
},
296294
domain="test-degree",
297295
)

scenarios/examples/multitenancy/example.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,14 @@ async def main():
4242
response=CreateWalletResponse,
4343
)
4444

45-
async with Controller(
46-
base_url=AGENCY, wallet_id=alice.wallet_id, subwallet_token=alice.token
47-
) as alice, Controller(
48-
base_url=AGENCY, wallet_id=bob.wallet_id, subwallet_token=bob.token
49-
) as bob:
45+
async with (
46+
Controller(
47+
base_url=AGENCY, wallet_id=alice.wallet_id, subwallet_token=alice.token
48+
) as alice,
49+
Controller(
50+
base_url=AGENCY, wallet_id=bob.wallet_id, subwallet_token=bob.token
51+
) as bob,
52+
):
5053
# Issuance prep
5154
config = (await alice.get("/status/config"))["config"]
5255
genesis_url = config.get("ledger.genesis_url")

0 commit comments

Comments
 (0)