doublezero-serviceability 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doublezero_serviceability-0.0.2.dist-info/METADATA +9 -0
- doublezero_serviceability-0.0.2.dist-info/RECORD +14 -0
- doublezero_serviceability-0.0.2.dist-info/WHEEL +4 -0
- serviceability/__init__.py +0 -0
- serviceability/client.py +85 -0
- serviceability/config.py +15 -0
- serviceability/pda.py +20 -0
- serviceability/rpc.py +48 -0
- serviceability/state.py +832 -0
- serviceability/tests/__init__.py +0 -0
- serviceability/tests/test_compat.py +145 -0
- serviceability/tests/test_enum_strings.py +110 -0
- serviceability/tests/test_fixtures.py +330 -0
- serviceability/tests/test_pda.py +36 -0
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Mainnet compatibility tests.
|
|
2
|
+
|
|
3
|
+
These tests fetch live mainnet-beta data and verify that our struct
|
|
4
|
+
deserialization works against real on-chain accounts.
|
|
5
|
+
|
|
6
|
+
Run with:
|
|
7
|
+
SERVICEABILITY_COMPAT_TEST=1 cd sdk/serviceability/python && uv run pytest -k compat -v
|
|
8
|
+
|
|
9
|
+
Requires network access to Solana mainnet RPC.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import struct
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
17
|
+
|
|
18
|
+
from serviceability.config import PROGRAM_IDS, LEDGER_RPC_URLS
|
|
19
|
+
from serviceability.pda import (
|
|
20
|
+
derive_global_config_pda,
|
|
21
|
+
derive_global_state_pda,
|
|
22
|
+
derive_program_config_pda,
|
|
23
|
+
)
|
|
24
|
+
from serviceability.state import GlobalConfig, GlobalState, ProgramConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def skip_unless_compat() -> None:
|
|
28
|
+
if not os.environ.get("SERVICEABILITY_COMPAT_TEST"):
|
|
29
|
+
pytest.skip("set SERVICEABILITY_COMPAT_TEST=1 to run compatibility tests against mainnet")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _rpc_url() -> str:
|
|
33
|
+
return os.environ.get("SOLANA_RPC_URL", LEDGER_RPC_URLS["mainnet-beta"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _program_id() -> Pubkey:
|
|
37
|
+
return Pubkey.from_string(PROGRAM_IDS["mainnet-beta"])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fetch_raw_account(addr: Pubkey) -> bytes:
|
|
41
|
+
from serviceability.rpc import new_rpc_client
|
|
42
|
+
|
|
43
|
+
rpc = new_rpc_client(_rpc_url())
|
|
44
|
+
resp = rpc.get_account_info(addr)
|
|
45
|
+
assert resp.value is not None, f"account not found: {addr}"
|
|
46
|
+
return bytes(resp.value.data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_u8(raw: bytes, offset: int) -> int:
|
|
50
|
+
return raw[offset]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_u16(raw: bytes, offset: int) -> int:
|
|
54
|
+
return struct.unpack_from("<H", raw, offset)[0]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_u32(raw: bytes, offset: int) -> int:
|
|
58
|
+
return struct.unpack_from("<I", raw, offset)[0]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def read_pubkey(raw: bytes, offset: int) -> Pubkey:
|
|
62
|
+
return Pubkey.from_bytes(raw[offset : offset + 32])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestCompatProgramConfig:
|
|
66
|
+
def test_deserialize(self) -> None:
|
|
67
|
+
skip_unless_compat()
|
|
68
|
+
|
|
69
|
+
program_id = _program_id()
|
|
70
|
+
addr, _ = derive_program_config_pda(program_id)
|
|
71
|
+
raw = fetch_raw_account(addr)
|
|
72
|
+
|
|
73
|
+
pc = ProgramConfig.from_bytes(raw)
|
|
74
|
+
|
|
75
|
+
# ProgramConfig layout (all fixed-size):
|
|
76
|
+
# offset 0: AccountType (u8)
|
|
77
|
+
# offset 1: BumpSeed (u8)
|
|
78
|
+
# offset 2: Version.Major (u32)
|
|
79
|
+
# offset 6: Version.Minor (u32)
|
|
80
|
+
# offset 10: Version.Patch (u32)
|
|
81
|
+
# offset 14: MinCompatVersion.Major (u32)
|
|
82
|
+
# offset 18: MinCompatVersion.Minor (u32)
|
|
83
|
+
# offset 22: MinCompatVersion.Patch (u32)
|
|
84
|
+
assert pc.account_type == read_u8(raw, 0), "AccountType"
|
|
85
|
+
assert pc.bump_seed == read_u8(raw, 1), "BumpSeed"
|
|
86
|
+
assert pc.version.major == read_u32(raw, 2), "Version.Major"
|
|
87
|
+
assert pc.version.minor == read_u32(raw, 6), "Version.Minor"
|
|
88
|
+
assert pc.version.patch == read_u32(raw, 10), "Version.Patch"
|
|
89
|
+
assert pc.min_compat_version.major == read_u32(raw, 14), "MinCompatVersion.Major"
|
|
90
|
+
assert pc.min_compat_version.minor == read_u32(raw, 18), "MinCompatVersion.Minor"
|
|
91
|
+
assert pc.min_compat_version.patch == read_u32(raw, 22), "MinCompatVersion.Patch"
|
|
92
|
+
|
|
93
|
+
assert pc.account_type == 9
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestCompatGlobalConfig:
|
|
97
|
+
def test_deserialize(self) -> None:
|
|
98
|
+
skip_unless_compat()
|
|
99
|
+
|
|
100
|
+
program_id = _program_id()
|
|
101
|
+
addr, _ = derive_global_config_pda(program_id)
|
|
102
|
+
raw = fetch_raw_account(addr)
|
|
103
|
+
|
|
104
|
+
gc = GlobalConfig.from_bytes(raw)
|
|
105
|
+
|
|
106
|
+
# GlobalConfig layout (all fixed-size):
|
|
107
|
+
# offset 0: AccountType (u8)
|
|
108
|
+
# offset 1: Owner (32 bytes)
|
|
109
|
+
# offset 33: BumpSeed (u8)
|
|
110
|
+
# offset 34: LocalASN (u32)
|
|
111
|
+
# offset 38: RemoteASN (u32)
|
|
112
|
+
# offset 57: NextBGPCommunity (u16)
|
|
113
|
+
assert gc.account_type == read_u8(raw, 0), "AccountType"
|
|
114
|
+
assert gc.owner == read_pubkey(raw, 1), "Owner"
|
|
115
|
+
assert gc.bump_seed == read_u8(raw, 33), "BumpSeed"
|
|
116
|
+
assert gc.local_asn == read_u32(raw, 34), "LocalASN"
|
|
117
|
+
assert gc.remote_asn == read_u32(raw, 38), "RemoteASN"
|
|
118
|
+
assert gc.next_bgp_community == read_u16(raw, 57), "NextBGPCommunity"
|
|
119
|
+
|
|
120
|
+
assert gc.account_type == 2
|
|
121
|
+
assert gc.local_asn > 0, "LocalASN should be > 0 on mainnet"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestCompatGlobalState:
|
|
125
|
+
def test_deserialize(self) -> None:
|
|
126
|
+
skip_unless_compat()
|
|
127
|
+
|
|
128
|
+
program_id = _program_id()
|
|
129
|
+
addr, _ = derive_global_state_pda(program_id)
|
|
130
|
+
raw = fetch_raw_account(addr)
|
|
131
|
+
|
|
132
|
+
gs = GlobalState.from_bytes(raw)
|
|
133
|
+
|
|
134
|
+
# GlobalState fixed layout (first 18 bytes before variable-length vecs):
|
|
135
|
+
# offset 0: AccountType (u8)
|
|
136
|
+
# offset 1: BumpSeed (u8)
|
|
137
|
+
assert gs.account_type == read_u8(raw, 0), "AccountType"
|
|
138
|
+
assert gs.bump_seed == read_u8(raw, 1), "BumpSeed"
|
|
139
|
+
|
|
140
|
+
assert gs.account_type == 1
|
|
141
|
+
|
|
142
|
+
# Sanity checks.
|
|
143
|
+
assert gs.activator_authority_pk != Pubkey.default(), "ActivatorAuthorityPK is zero"
|
|
144
|
+
assert gs.sentinel_authority_pk != Pubkey.default(), "SentinelAuthorityPK is zero"
|
|
145
|
+
# health_oracle_pk may be zero on mainnet
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Test that enum __str__ outputs match the shared fixture file."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from serviceability.state import (
|
|
9
|
+
AccessPassStatus,
|
|
10
|
+
AccessPassTypeTag,
|
|
11
|
+
ContributorStatus,
|
|
12
|
+
CyoaType,
|
|
13
|
+
DeviceDesiredStatus,
|
|
14
|
+
DeviceDeviceType,
|
|
15
|
+
DeviceHealth,
|
|
16
|
+
DeviceStatus,
|
|
17
|
+
ExchangeStatus,
|
|
18
|
+
InterfaceStatus,
|
|
19
|
+
InterfaceType,
|
|
20
|
+
LinkDesiredStatus,
|
|
21
|
+
LinkHealth,
|
|
22
|
+
LinkLinkType,
|
|
23
|
+
LinkStatus,
|
|
24
|
+
LocationStatus,
|
|
25
|
+
LoopbackType,
|
|
26
|
+
MulticastGroupStatus,
|
|
27
|
+
UserStatus,
|
|
28
|
+
UserUserType,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
FIXTURE_PATH = Path(__file__).resolve().parent.parent.parent.parent / "testdata" / "enum_strings.json"
|
|
32
|
+
|
|
33
|
+
ENUM_MAP = {
|
|
34
|
+
"LocationStatus": LocationStatus,
|
|
35
|
+
"ExchangeStatus": ExchangeStatus,
|
|
36
|
+
"DeviceDeviceType": DeviceDeviceType,
|
|
37
|
+
"DeviceStatus": DeviceStatus,
|
|
38
|
+
"DeviceHealth": DeviceHealth,
|
|
39
|
+
"DeviceDesiredStatus": DeviceDesiredStatus,
|
|
40
|
+
"InterfaceStatus": InterfaceStatus,
|
|
41
|
+
"InterfaceType": InterfaceType,
|
|
42
|
+
"LoopbackType": LoopbackType,
|
|
43
|
+
"LinkLinkType": LinkLinkType,
|
|
44
|
+
"LinkStatus": LinkStatus,
|
|
45
|
+
"LinkHealth": LinkHealth,
|
|
46
|
+
"LinkDesiredStatus": LinkDesiredStatus,
|
|
47
|
+
"ContributorStatus": ContributorStatus,
|
|
48
|
+
"UserUserType": UserUserType,
|
|
49
|
+
"CyoaType": CyoaType,
|
|
50
|
+
"UserStatus": UserStatus,
|
|
51
|
+
"MulticastGroupStatus": MulticastGroupStatus,
|
|
52
|
+
"AccessPassTypeTag": AccessPassTypeTag,
|
|
53
|
+
"AccessPassStatus": AccessPassStatus,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load_fixture() -> dict:
|
|
58
|
+
return json.loads(FIXTURE_PATH.read_text())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _enum_test_cases() -> list[tuple[str, int, str]]:
|
|
62
|
+
"""Yield (enum_name, int_value, expected_string) tuples."""
|
|
63
|
+
fixture = _load_fixture()
|
|
64
|
+
cases = []
|
|
65
|
+
for enum_name, mappings in fixture.items():
|
|
66
|
+
if enum_name not in ENUM_MAP:
|
|
67
|
+
continue
|
|
68
|
+
for str_value, expected in mappings.items():
|
|
69
|
+
cases.append((enum_name, int(str_value), expected))
|
|
70
|
+
return cases
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parametrize(
|
|
74
|
+
"enum_name,int_value,expected",
|
|
75
|
+
_enum_test_cases(),
|
|
76
|
+
ids=lambda x: str(x),
|
|
77
|
+
)
|
|
78
|
+
def test_enum_str(enum_name: str, int_value: int, expected: str) -> None:
|
|
79
|
+
enum_cls = ENUM_MAP[enum_name]
|
|
80
|
+
try:
|
|
81
|
+
instance = enum_cls(int_value)
|
|
82
|
+
except ValueError:
|
|
83
|
+
# Unknown value not defined as a member; skip since Python IntEnum
|
|
84
|
+
# cannot instantiate undefined members. Go/TS tests cover this case.
|
|
85
|
+
pytest.skip(f"{enum_name}({int_value}) is not a valid member")
|
|
86
|
+
return
|
|
87
|
+
assert str(instance) == expected, (
|
|
88
|
+
f"{enum_name}({int_value}): expected {expected!r}, got {str(instance)!r}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_all_enum_members_in_fixture() -> None:
|
|
93
|
+
"""Every Python IntEnum member must appear in the fixture.
|
|
94
|
+
|
|
95
|
+
This catches the case where a variant is added in Python but not in
|
|
96
|
+
enum_strings.json. Since the fixture is shared across Go, Python, and
|
|
97
|
+
TypeScript, updating it will cause the other languages' tests to fail
|
|
98
|
+
until they add the new variant too.
|
|
99
|
+
"""
|
|
100
|
+
fixture = _load_fixture()
|
|
101
|
+
missing = []
|
|
102
|
+
for enum_name, enum_cls in ENUM_MAP.items():
|
|
103
|
+
fixture_values = fixture.get(enum_name, {})
|
|
104
|
+
for member in enum_cls:
|
|
105
|
+
if str(member.value) not in fixture_values:
|
|
106
|
+
missing.append(f"{enum_name}.{member.name} ({member.value})")
|
|
107
|
+
assert not missing, (
|
|
108
|
+
"Enum members missing from enum_strings.json fixture — add them so "
|
|
109
|
+
"other languages stay in sync:\n " + "\n ".join(missing)
|
|
110
|
+
)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Fixture-based compatibility tests."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
7
|
+
|
|
8
|
+
from serviceability.state import (
|
|
9
|
+
AccessPass,
|
|
10
|
+
Contributor,
|
|
11
|
+
Device,
|
|
12
|
+
Exchange,
|
|
13
|
+
GlobalConfig,
|
|
14
|
+
GlobalState,
|
|
15
|
+
Link,
|
|
16
|
+
Location,
|
|
17
|
+
MulticastGroup,
|
|
18
|
+
ProgramConfig,
|
|
19
|
+
User,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
FIXTURES_DIR = Path(__file__).resolve().parent.parent.parent.parent / "testdata" / "fixtures"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_fixture(name: str) -> tuple[bytes, dict]:
|
|
26
|
+
bin_data = (FIXTURES_DIR / f"{name}.bin").read_bytes()
|
|
27
|
+
meta = json.loads((FIXTURES_DIR / f"{name}.json").read_text())
|
|
28
|
+
return bin_data, meta
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _assert_fields(expected_fields: list[dict], got: dict) -> None:
|
|
32
|
+
for f in expected_fields:
|
|
33
|
+
name = f["name"]
|
|
34
|
+
if name not in got:
|
|
35
|
+
continue
|
|
36
|
+
typ = f["typ"]
|
|
37
|
+
raw = f["value"]
|
|
38
|
+
actual = got[name]
|
|
39
|
+
if typ in ("u8", "u16", "u32", "u64"):
|
|
40
|
+
assert actual == int(raw), f"{name}: expected {raw}, got {actual}"
|
|
41
|
+
elif typ == "pubkey":
|
|
42
|
+
expected = Pubkey.from_string(raw)
|
|
43
|
+
assert actual == expected, f"{name}: expected {expected}, got {actual}"
|
|
44
|
+
elif typ == "string":
|
|
45
|
+
assert actual == raw, f"{name}: expected {raw!r}, got {actual!r}"
|
|
46
|
+
elif typ == "bool":
|
|
47
|
+
expected = raw == "true"
|
|
48
|
+
assert actual == expected, f"{name}: expected {expected}, got {actual}"
|
|
49
|
+
elif typ == "ipv4":
|
|
50
|
+
import ipaddress
|
|
51
|
+
|
|
52
|
+
expected_bytes = ipaddress.IPv4Address(raw).packed
|
|
53
|
+
assert actual == expected_bytes, f"{name}: expected {raw}, got {actual}"
|
|
54
|
+
elif typ == "networkv4":
|
|
55
|
+
import ipaddress
|
|
56
|
+
|
|
57
|
+
net = ipaddress.IPv4Network(raw)
|
|
58
|
+
expected_bytes = net.network_address.packed + bytes([net.prefixlen])
|
|
59
|
+
assert actual == expected_bytes, f"{name}: expected {raw}, got {actual}"
|
|
60
|
+
elif typ == "u128":
|
|
61
|
+
assert actual == int(raw), f"{name}: expected {raw}, got {actual}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestFixtureGlobalState:
|
|
65
|
+
def test_deserialize(self):
|
|
66
|
+
data, meta = _load_fixture("global_state")
|
|
67
|
+
gs = GlobalState.from_bytes(data)
|
|
68
|
+
_assert_fields(
|
|
69
|
+
meta["fields"],
|
|
70
|
+
{
|
|
71
|
+
"AccountType": gs.account_type,
|
|
72
|
+
"BumpSeed": gs.bump_seed,
|
|
73
|
+
"ContributorAirdropLamports": gs.contributor_airdrop_lamports,
|
|
74
|
+
"UserAirdropLamports": gs.user_airdrop_lamports,
|
|
75
|
+
"ActivatorAuthorityPk": gs.activator_authority_pk,
|
|
76
|
+
"SentinelAuthorityPk": gs.sentinel_authority_pk,
|
|
77
|
+
"HealthOraclePk": gs.health_oracle_pk,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestFixtureGlobalConfig:
|
|
83
|
+
def test_deserialize(self):
|
|
84
|
+
data, meta = _load_fixture("global_config")
|
|
85
|
+
gc = GlobalConfig.from_bytes(data)
|
|
86
|
+
_assert_fields(
|
|
87
|
+
meta["fields"],
|
|
88
|
+
{
|
|
89
|
+
"AccountType": gc.account_type,
|
|
90
|
+
"Owner": gc.owner,
|
|
91
|
+
"BumpSeed": gc.bump_seed,
|
|
92
|
+
"LocalAsn": gc.local_asn,
|
|
93
|
+
"RemoteAsn": gc.remote_asn,
|
|
94
|
+
"NextBgpCommunity": gc.next_bgp_community,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestFixtureLocation:
|
|
100
|
+
def test_deserialize(self):
|
|
101
|
+
data, meta = _load_fixture("location")
|
|
102
|
+
loc = Location.from_bytes(data)
|
|
103
|
+
_assert_fields(
|
|
104
|
+
meta["fields"],
|
|
105
|
+
{
|
|
106
|
+
"AccountType": loc.account_type,
|
|
107
|
+
"Owner": loc.owner,
|
|
108
|
+
"BumpSeed": loc.bump_seed,
|
|
109
|
+
"LocId": loc.loc_id,
|
|
110
|
+
"Status": loc.status,
|
|
111
|
+
"ReferenceCount": loc.reference_count,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestFixtureExchange:
|
|
117
|
+
def test_deserialize(self):
|
|
118
|
+
data, meta = _load_fixture("exchange")
|
|
119
|
+
ex = Exchange.from_bytes(data)
|
|
120
|
+
_assert_fields(
|
|
121
|
+
meta["fields"],
|
|
122
|
+
{
|
|
123
|
+
"AccountType": ex.account_type,
|
|
124
|
+
"Owner": ex.owner,
|
|
125
|
+
"BumpSeed": ex.bump_seed,
|
|
126
|
+
"BgpCommunity": ex.bgp_community,
|
|
127
|
+
"Status": ex.status,
|
|
128
|
+
"ReferenceCount": ex.reference_count,
|
|
129
|
+
"Device1Pk": ex.device1_pk,
|
|
130
|
+
"Device2Pk": ex.device2_pk,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestFixtureDevice:
|
|
136
|
+
def test_deserialize(self):
|
|
137
|
+
data, meta = _load_fixture("device")
|
|
138
|
+
dev = Device.from_bytes(data)
|
|
139
|
+
_assert_fields(
|
|
140
|
+
meta["fields"],
|
|
141
|
+
{
|
|
142
|
+
"AccountType": dev.account_type,
|
|
143
|
+
"Owner": dev.owner,
|
|
144
|
+
"Index": dev.index,
|
|
145
|
+
"BumpSeed": dev.bump_seed,
|
|
146
|
+
"DeviceType": dev.device_type,
|
|
147
|
+
"PublicIp": dev.public_ip,
|
|
148
|
+
"Status": dev.status,
|
|
149
|
+
"Code": dev.code,
|
|
150
|
+
"MgmtVrf": dev.mgmt_vrf,
|
|
151
|
+
"ReferenceCount": dev.reference_count,
|
|
152
|
+
"UsersCount": dev.users_count,
|
|
153
|
+
"MaxUsers": dev.max_users,
|
|
154
|
+
"DeviceHealth": dev.device_health,
|
|
155
|
+
"DesiredStatus": dev.device_desired_status,
|
|
156
|
+
"MetricsPublisherPk": dev.metrics_publisher_pub_key,
|
|
157
|
+
"ContributorPk": dev.contributor_pub_key,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
# Verify interfaces
|
|
161
|
+
assert len(dev.interfaces) == 2
|
|
162
|
+
assert dev.interfaces[0].name == "Loopback0"
|
|
163
|
+
assert dev.interfaces[1].name == "Ethernet1"
|
|
164
|
+
# Verify dz_prefixes
|
|
165
|
+
import ipaddress
|
|
166
|
+
|
|
167
|
+
assert len(dev.dz_prefixes) == 1
|
|
168
|
+
net = ipaddress.IPv4Network("10.10.0.0/24")
|
|
169
|
+
expected_prefix = net.network_address.packed + bytes([net.prefixlen])
|
|
170
|
+
assert dev.dz_prefixes[0] == expected_prefix
|
|
171
|
+
# Verify code, mgmt_vrf, public_ip, index
|
|
172
|
+
assert dev.code == "dz1"
|
|
173
|
+
assert dev.mgmt_vrf == "mgmt"
|
|
174
|
+
assert dev.public_ip == ipaddress.IPv4Address("203.0.113.1").packed
|
|
175
|
+
assert dev.index == 7
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestFixtureLink:
|
|
179
|
+
def test_deserialize(self):
|
|
180
|
+
data, meta = _load_fixture("link")
|
|
181
|
+
lk = Link.from_bytes(data)
|
|
182
|
+
_assert_fields(
|
|
183
|
+
meta["fields"],
|
|
184
|
+
{
|
|
185
|
+
"AccountType": lk.account_type,
|
|
186
|
+
"Owner": lk.owner,
|
|
187
|
+
"BumpSeed": lk.bump_seed,
|
|
188
|
+
"LinkType": lk.link_type,
|
|
189
|
+
"Bandwidth": lk.bandwidth,
|
|
190
|
+
"Mtu": lk.mtu,
|
|
191
|
+
"DelayNs": lk.delay_ns,
|
|
192
|
+
"JitterNs": lk.jitter_ns,
|
|
193
|
+
"TunnelId": lk.tunnel_id,
|
|
194
|
+
"Status": lk.status,
|
|
195
|
+
"ContributorPk": lk.contributor_pub_key,
|
|
196
|
+
"DelayOverrideNs": lk.delay_override_ns,
|
|
197
|
+
"LinkHealth": lk.link_health,
|
|
198
|
+
"DesiredStatus": lk.link_desired_status,
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestFixtureUser:
|
|
204
|
+
def test_deserialize(self):
|
|
205
|
+
data, meta = _load_fixture("user")
|
|
206
|
+
u = User.from_bytes(data)
|
|
207
|
+
_assert_fields(
|
|
208
|
+
meta["fields"],
|
|
209
|
+
{
|
|
210
|
+
"AccountType": u.account_type,
|
|
211
|
+
"Owner": u.owner,
|
|
212
|
+
"BumpSeed": u.bump_seed,
|
|
213
|
+
"UserType": u.user_type,
|
|
214
|
+
"TenantPk": u.tenant_pub_key,
|
|
215
|
+
"DevicePk": u.device_pub_key,
|
|
216
|
+
"CyoaType": u.cyoa_type,
|
|
217
|
+
"TunnelId": u.tunnel_id,
|
|
218
|
+
"Status": u.status,
|
|
219
|
+
"ValidatorPubkey": u.validator_pub_key,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TestFixtureMulticastGroup:
|
|
225
|
+
def test_deserialize(self):
|
|
226
|
+
data, meta = _load_fixture("multicast_group")
|
|
227
|
+
mg = MulticastGroup.from_bytes(data)
|
|
228
|
+
_assert_fields(
|
|
229
|
+
meta["fields"],
|
|
230
|
+
{
|
|
231
|
+
"AccountType": mg.account_type,
|
|
232
|
+
"Owner": mg.owner,
|
|
233
|
+
"BumpSeed": mg.bump_seed,
|
|
234
|
+
"TenantPk": mg.tenant_pub_key,
|
|
235
|
+
"MaxBandwidth": mg.max_bandwidth,
|
|
236
|
+
"Status": mg.status,
|
|
237
|
+
"PublisherCount": mg.publisher_count,
|
|
238
|
+
"SubscriberCount": mg.subscriber_count,
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TestFixtureContributor:
|
|
244
|
+
def test_deserialize(self):
|
|
245
|
+
data, meta = _load_fixture("contributor")
|
|
246
|
+
c = Contributor.from_bytes(data)
|
|
247
|
+
_assert_fields(
|
|
248
|
+
meta["fields"],
|
|
249
|
+
{
|
|
250
|
+
"AccountType": c.account_type,
|
|
251
|
+
"Owner": c.owner,
|
|
252
|
+
"BumpSeed": c.bump_seed,
|
|
253
|
+
"Status": c.status,
|
|
254
|
+
"ReferenceCount": c.reference_count,
|
|
255
|
+
"OpsManagerPk": c.ops_manager_pk,
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestFixtureProgramConfig:
|
|
261
|
+
def test_deserialize(self):
|
|
262
|
+
data, meta = _load_fixture("program_config")
|
|
263
|
+
pc = ProgramConfig.from_bytes(data)
|
|
264
|
+
_assert_fields(
|
|
265
|
+
meta["fields"],
|
|
266
|
+
{
|
|
267
|
+
"AccountType": pc.account_type,
|
|
268
|
+
"BumpSeed": pc.bump_seed,
|
|
269
|
+
"VersionMajor": pc.version.major,
|
|
270
|
+
"VersionMinor": pc.version.minor,
|
|
271
|
+
"VersionPatch": pc.version.patch,
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestFixtureAccessPass:
|
|
277
|
+
def test_deserialize(self):
|
|
278
|
+
data, meta = _load_fixture("access_pass")
|
|
279
|
+
ap = AccessPass.from_bytes(data)
|
|
280
|
+
_assert_fields(
|
|
281
|
+
meta["fields"],
|
|
282
|
+
{
|
|
283
|
+
"AccountType": ap.account_type,
|
|
284
|
+
"Owner": ap.owner,
|
|
285
|
+
"BumpSeed": ap.bump_seed,
|
|
286
|
+
"AccessPassType": ap.access_pass_type_tag,
|
|
287
|
+
"UserPayer": ap.user_payer,
|
|
288
|
+
"LastAccessEpoch": ap.last_access_epoch,
|
|
289
|
+
"ConnectionCount": ap.connection_count,
|
|
290
|
+
"Status": ap.status,
|
|
291
|
+
"Flags": ap.flags,
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestFixtureAccessPassValidator:
|
|
297
|
+
def test_deserialize(self):
|
|
298
|
+
data, meta = _load_fixture("access_pass_validator")
|
|
299
|
+
ap = AccessPass.from_bytes(data)
|
|
300
|
+
_assert_fields(
|
|
301
|
+
meta["fields"],
|
|
302
|
+
{
|
|
303
|
+
"AccountType": ap.account_type,
|
|
304
|
+
"Owner": ap.owner,
|
|
305
|
+
"BumpSeed": ap.bump_seed,
|
|
306
|
+
"AccessPassType": ap.access_pass_type_tag,
|
|
307
|
+
"AccessPassTypeValidatorPubkey": ap.validator_pub_key,
|
|
308
|
+
"ClientIp": ap.client_ip,
|
|
309
|
+
"UserPayer": ap.user_payer,
|
|
310
|
+
"LastAccessEpoch": ap.last_access_epoch,
|
|
311
|
+
"ConnectionCount": ap.connection_count,
|
|
312
|
+
"Status": ap.status,
|
|
313
|
+
"Flags": ap.flags,
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
assert ap.account_type == 11
|
|
317
|
+
assert ap.bump_seed == 243
|
|
318
|
+
assert ap.access_pass_type_tag == 1
|
|
319
|
+
assert ap.validator_pub_key == Pubkey.from_string(
|
|
320
|
+
"BuP3jEYfnTCfB4UqQk9L37k2vaXsNuVsbWxrYbGDmL6s"
|
|
321
|
+
)
|
|
322
|
+
import ipaddress
|
|
323
|
+
|
|
324
|
+
assert ap.client_ip == ipaddress.IPv4Address("10.0.0.50").packed
|
|
325
|
+
assert ap.last_access_epoch == 1000
|
|
326
|
+
assert ap.connection_count == 1
|
|
327
|
+
assert ap.status == 1
|
|
328
|
+
assert len(ap.mgroup_pub_allowlist) == 1
|
|
329
|
+
assert len(ap.mgroup_sub_allowlist) == 1
|
|
330
|
+
assert ap.flags == 3
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from solders.pubkey import Pubkey # type: ignore[import-untyped]
|
|
2
|
+
|
|
3
|
+
from serviceability.pda import (
|
|
4
|
+
derive_global_config_pda,
|
|
5
|
+
derive_global_state_pda,
|
|
6
|
+
derive_program_config_pda,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
PROGRAM_ID = Pubkey.from_string("ser2VaTMAcYTaauMrTSfSrxBaUDq7BLNs2xfUugTAGv")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_derive_global_state_pda():
|
|
13
|
+
addr, bump = derive_global_state_pda(PROGRAM_ID)
|
|
14
|
+
assert addr != Pubkey.default()
|
|
15
|
+
addr2, bump2 = derive_global_state_pda(PROGRAM_ID)
|
|
16
|
+
assert addr == addr2
|
|
17
|
+
assert bump == bump2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_derive_global_config_pda():
|
|
21
|
+
addr, _ = derive_global_config_pda(PROGRAM_ID)
|
|
22
|
+
assert addr != Pubkey.default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_derive_program_config_pda():
|
|
26
|
+
addr, _ = derive_program_config_pda(PROGRAM_ID)
|
|
27
|
+
assert addr != Pubkey.default()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_pdas_are_different():
|
|
31
|
+
gs, _ = derive_global_state_pda(PROGRAM_ID)
|
|
32
|
+
gc, _ = derive_global_config_pda(PROGRAM_ID)
|
|
33
|
+
pc, _ = derive_program_config_pda(PROGRAM_ID)
|
|
34
|
+
assert gs != gc
|
|
35
|
+
assert gs != pc
|
|
36
|
+
assert gc != pc
|