Skip to content

Commit 4f6410a

Browse files
committed
test: add TLS 1.3 integration tests
1 parent 8054ab7 commit 4f6410a

2 files changed

Lines changed: 301 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# --------------------------------------------------------------------------------------------
6+
7+
"""
8+
Integration tests for TLS 1.3 DPS linked hub features.
9+
10+
Tests cover:
11+
- Linked hub create with hostname types (auto, classic, device)
12+
- Hostname resolution for GWv2 hubs
13+
- Linked hub list after creation
14+
"""
15+
16+
import pytest
17+
from azext_iot.common.embedded_cli import EmbeddedCLI
18+
19+
cli = EmbeddedCLI()
20+
21+
22+
def _find_gwv2_hub(rg):
23+
"""Find a GWv2 hub in the RG"""
24+
hubs = cli.invoke(f"iot hub list -g {rg}").as_json()
25+
for hub in hubs:
26+
if hub.get("properties", {}).get("deviceHostName"):
27+
return hub
28+
return None
29+
30+
31+
def _cleanup_linked_hub(dps_name, rg, linked_hub_name):
32+
cli.invoke(
33+
f"iot dps linked-hub delete --dps-name {dps_name} -g {rg} "
34+
f"--linked-hub {linked_hub_name}"
35+
)
36+
37+
38+
def test_linked_hub_create_auto_hostname(provisioned_iot_dps_no_hub_module):
39+
"""On a GWv2 hub, auto (default) should resolve to the device hostname."""
40+
dps_name = provisioned_iot_dps_no_hub_module["name"]
41+
dps_rg = provisioned_iot_dps_no_hub_module["resourceGroup"]
42+
43+
gwv2_hub = _find_gwv2_hub(dps_rg)
44+
if not gwv2_hub:
45+
pytest.skip("No GWv2 hub available in resource group for TLS 1.3 testing")
46+
47+
hub_name = gwv2_hub["name"]
48+
device_hostname = gwv2_hub["properties"]["deviceHostName"]
49+
50+
try:
51+
result = cli.invoke(
52+
f"iot dps linked-hub create --dps-name {dps_name} -g {dps_rg} "
53+
f"--hub-name {hub_name}"
54+
).as_json()
55+
56+
assert result, "Linked hub create should return a result"
57+
linked_hubs = result if isinstance(result, list) else [result]
58+
matching = [h for h in linked_hubs if h["name"] == device_hostname]
59+
assert len(matching) == 1, \
60+
f"Expected linked hub with name '{device_hostname}'. Got: {[h['name'] for h in linked_hubs]}"
61+
assert device_hostname in matching[0]["connectionString"], \
62+
"Connection string should use device hostname"
63+
finally:
64+
_cleanup_linked_hub(dps_name, dps_rg, device_hostname)
65+
66+
67+
def test_linked_hub_create_classic_hostname(provisioned_iot_dps_no_hub_module):
68+
"""Create linked hub with explicit classic hostname type."""
69+
dps_name = provisioned_iot_dps_no_hub_module["name"]
70+
dps_rg = provisioned_iot_dps_no_hub_module["resourceGroup"]
71+
72+
gwv2_hub = _find_gwv2_hub(dps_rg)
73+
if not gwv2_hub:
74+
pytest.skip("No GWv2 hub available in resource group")
75+
76+
hub_name = gwv2_hub["name"]
77+
classic_hostname = gwv2_hub["properties"]["hostName"]
78+
79+
try:
80+
result = cli.invoke(
81+
f"iot dps linked-hub create --dps-name {dps_name} -g {dps_rg} "
82+
f"--hub-name {hub_name} --hostname-type classic"
83+
).as_json()
84+
85+
assert result, "Linked hub create should return a result"
86+
linked_hubs = result if isinstance(result, list) else [result]
87+
matching = [h for h in linked_hubs if h["name"] == classic_hostname]
88+
assert len(matching) == 1, \
89+
f"Expected linked hub with name '{classic_hostname}'. Got: {[h['name'] for h in linked_hubs]}"
90+
assert ".device." not in matching[0]["name"], \
91+
"Classic hostname should not contain .device. segment"
92+
finally:
93+
_cleanup_linked_hub(dps_name, dps_rg, classic_hostname)
94+
95+
96+
def test_linked_hub_create_device_hostname(provisioned_iot_dps_no_hub_module):
97+
"""Create linked hub with explicit device hostname type on a GWv2 hub."""
98+
dps_name = provisioned_iot_dps_no_hub_module["name"]
99+
dps_rg = provisioned_iot_dps_no_hub_module["resourceGroup"]
100+
101+
gwv2_hub = _find_gwv2_hub(dps_rg)
102+
if not gwv2_hub:
103+
pytest.skip("No GWv2 hub available in resource group")
104+
105+
hub_name = gwv2_hub["name"]
106+
device_hostname = gwv2_hub["properties"]["deviceHostName"]
107+
108+
try:
109+
result = cli.invoke(
110+
f"iot dps linked-hub create --dps-name {dps_name} -g {dps_rg} "
111+
f"--hub-name {hub_name} --hostname-type device"
112+
).as_json()
113+
114+
assert result, "Linked hub create should return a result"
115+
linked_hubs = result if isinstance(result, list) else [result]
116+
matching = [h for h in linked_hubs if h["name"] == device_hostname]
117+
assert len(matching) == 1, \
118+
f"Expected linked hub with name '{device_hostname}'. Got: {[h['name'] for h in linked_hubs]}"
119+
assert ".device." in matching[0]["name"], \
120+
"Device hostname should contain .device. segment"
121+
finally:
122+
_cleanup_linked_hub(dps_name, dps_rg, device_hostname)
123+
124+
125+
def test_hub_show_returns_tls13_hostnames(provisioned_iot_dps_no_hub_module):
126+
"""Verify hub show returns TLS 1.3 hostname properties for GWv2 hubs."""
127+
dps_rg = provisioned_iot_dps_no_hub_module["resourceGroup"]
128+
129+
gwv2_hub = _find_gwv2_hub(dps_rg)
130+
if not gwv2_hub:
131+
pytest.skip("No GWv2 hub available in resource group")
132+
133+
hub_name = gwv2_hub["name"]
134+
result = cli.invoke(f"iot hub show -n {hub_name}").as_json()
135+
props = result["properties"]
136+
137+
assert props.get("hostName"), "hostName (classic) should be present"
138+
assert props.get("deviceHostName"), "deviceHostName should be present for GWv2 hub"
139+
assert props.get("serviceHostName"), "serviceHostName should be present for GWv2 hub"
140+
141+
assert ".device." in props["deviceHostName"]
142+
assert ".service." in props["serviceHostName"]
143+
assert hub_name in props["hostName"]
144+
assert hub_name in props["deviceHostName"]
145+
assert hub_name in props["serviceHostName"]
146+
147+
148+
def test_linked_hub_list_shows_hostname(provisioned_iot_dps_no_hub_module):
149+
"""Verify linked hub list returns the correct hostname after linking."""
150+
dps_name = provisioned_iot_dps_no_hub_module["name"]
151+
dps_rg = provisioned_iot_dps_no_hub_module["resourceGroup"]
152+
153+
gwv2_hub = _find_gwv2_hub(dps_rg)
154+
if not gwv2_hub:
155+
pytest.skip("No GWv2 hub available in resource group")
156+
157+
hub_name = gwv2_hub["name"]
158+
device_hostname = gwv2_hub["properties"]["deviceHostName"]
159+
160+
try:
161+
cli.invoke(
162+
f"iot dps linked-hub create --dps-name {dps_name} -g {dps_rg} "
163+
f"--hub-name {hub_name}"
164+
)
165+
166+
linked_hubs = cli.invoke(
167+
f"iot dps linked-hub list --dps-name {dps_name} -g {dps_rg}"
168+
).as_json()
169+
170+
matching = [h for h in linked_hubs if h["name"] == device_hostname]
171+
assert len(matching) == 1, \
172+
f"Should find linked hub with name '{device_hostname}'. Found: {[h['name'] for h in linked_hubs]}"
173+
finally:
174+
_cleanup_linked_hub(dps_name, dps_rg, device_hostname)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# --------------------------------------------------------------------------------------------
6+
7+
"""
8+
Integration tests for IoT Hub discovery and hostname resolution.
9+
10+
Tests cover:
11+
- Hub discovery returns TLS 1.3 hostname properties
12+
- Service hostname used for data plane target on GWv2 hubs
13+
- Connection string uses correct hostname based on GWv2 status
14+
"""
15+
16+
import pytest
17+
from azure.cli.testsdk.reverse_dependency import get_dummy_cli
18+
from azext_iot.common.embedded_cli import EmbeddedCLI
19+
from azext_iot.iothub.providers.discovery import IotHubDiscovery
20+
from azext_iot.tests.settings import (
21+
DynamoSettings,
22+
Setting,
23+
ENV_SET_TEST_IOTHUB_REQUIRED,
24+
ENV_SET_TEST_IOTHUB_OPTIONAL,
25+
)
26+
27+
cli = EmbeddedCLI()
28+
settings = DynamoSettings(
29+
req_env_set=ENV_SET_TEST_IOTHUB_REQUIRED,
30+
opt_env_set=ENV_SET_TEST_IOTHUB_OPTIONAL,
31+
)
32+
ENTITY_RG = settings.env.azext_iot_testrg
33+
ENTITY_NAME = settings.env.azext_iot_testhub
34+
35+
36+
@pytest.fixture(scope="module")
37+
def discovery():
38+
cmd_shell = Setting()
39+
setattr(cmd_shell, "cli_ctx", get_dummy_cli())
40+
return IotHubDiscovery(cmd_shell)
41+
42+
43+
def test_find_resource_returns_hostname_properties(discovery):
44+
"""Verify find_resource returns TLS 1.3 hostname properties."""
45+
if not ENTITY_NAME:
46+
pytest.skip("azext_iot_testhub not set")
47+
48+
resource = discovery.find_resource(resource_name=ENTITY_NAME)
49+
props = resource.get("properties", {})
50+
51+
assert props.get("hostName"), "hostName (classic) should be present"
52+
53+
device_hostname = props.get("deviceHostName")
54+
service_hostname = props.get("serviceHostName")
55+
if device_hostname:
56+
assert ENTITY_NAME in device_hostname
57+
assert ".device." in device_hostname
58+
if service_hostname:
59+
assert ENTITY_NAME in service_hostname
60+
assert ".service." in service_hostname
61+
62+
63+
def test_build_target_includes_hostname_fields(discovery):
64+
"""Verify _build_target populates deviceHostName and serviceHostName."""
65+
if not ENTITY_NAME:
66+
pytest.skip("azext_iot_testhub not set")
67+
68+
resource = discovery.find_resource(resource_name=ENTITY_NAME)
69+
policy = discovery.find_policy(resource_name=ENTITY_NAME, rg=ENTITY_RG)
70+
target = discovery._build_target(resource=resource, policy=policy)
71+
72+
assert target.get("entity"), "entity (hostname) should be set"
73+
assert target.get("cs"), "connection string should be set"
74+
assert "deviceHostName" in target, "target should include deviceHostName key"
75+
assert "serviceHostName" in target, "target should include serviceHostName key"
76+
77+
78+
def test_gwv2_target_uses_service_hostname(discovery):
79+
"""For GWv2 hubs, _build_target should use service hostname for entity."""
80+
if not ENTITY_NAME:
81+
pytest.skip("azext_iot_testhub not set")
82+
83+
resource = discovery.find_resource(resource_name=ENTITY_NAME)
84+
props = resource.get("properties", {})
85+
gw_version = props.get("iotHubDetails", {}).get("gatewayVersion")
86+
service_hostname = props.get("serviceHostName")
87+
88+
if gw_version != "V2" or not service_hostname:
89+
pytest.skip("Hub is not GWv2 — skipping service hostname test")
90+
91+
policy = discovery.find_policy(resource_name=ENTITY_NAME, rg=ENTITY_RG)
92+
target = discovery._build_target(resource=resource, policy=policy)
93+
94+
assert target["entity"] == service_hostname, \
95+
f"GWv2 hub entity should be service hostname. Got: {target['entity']}"
96+
assert service_hostname in target["cs"], \
97+
"Connection string should use service hostname for GWv2 hub"
98+
99+
100+
def test_connection_string_uses_gwv2_hostname():
101+
"""Verify connection-string show uses the device hostname for GWv2 hubs."""
102+
if not ENTITY_NAME:
103+
pytest.skip("azext_iot_testhub not set")
104+
105+
hub = cli.invoke(f"iot hub show -n {ENTITY_NAME}").as_json()
106+
props = hub.get("properties", {})
107+
device_hostname = props.get("deviceHostName")
108+
109+
result = cli.invoke(f"iot hub connection-string show -n {ENTITY_NAME}").as_json()
110+
cs = result["connectionString"]
111+
assert "HostName=" in cs
112+
113+
cs_hostname = None
114+
for part in cs.split(";"):
115+
if part.startswith("HostName="):
116+
cs_hostname = part.split("=", 1)[1]
117+
break
118+
119+
assert cs_hostname, "Should extract hostname from connection string"
120+
121+
if device_hostname:
122+
assert cs_hostname == device_hostname, \
123+
f"GWv2 connection string should use device hostname '{device_hostname}'. Got: '{cs_hostname}'"
124+
else:
125+
classic_hostname = props.get("hostName")
126+
assert cs_hostname == classic_hostname, \
127+
f"V1 connection string should use classic hostname '{classic_hostname}'. Got: '{cs_hostname}'"

0 commit comments

Comments
 (0)