[PAVST] Add TC_PAVST_2_10.py for URL validation (#41362)
* Add TC_PAVST_2_10.py for URL validation.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Address Geminibot reiew comments and clean up.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Restyled by autopep8
* Restyled by isort
* Address LINT errors and review comments.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Move URL validation to SDK.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Address review comments.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Update TCs for URL validation.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Pass expected cluster status.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Restyled by autopep8
* Fix Lint Error
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Updating status code to valid code
Co-author: Sambhavi<sambhavi.1@samsung.com>
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Fix build failure
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Fix build failure
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
* Fix CI failure.
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
---------
Signed-off-by: Raveendra Karu <r.karu@samsung.com>
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h b/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h
index 227b8c8..3d118a0 100644
--- a/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h
+++ b/examples/all-clusters-app/all-clusters-common/include/push-av-stream-transport-delegate-impl.h
@@ -56,8 +56,6 @@
const uint16_t connectionID, TriggerActivationReasonEnum activationReason,
const Optional<Structs::TransportMotionTriggerTimeControlStruct::DecodableType> & timeControl) override;
- bool ValidateUrl(const std::string & url) override;
-
Protocols::InteractionModel::Status
ValidateBandwidthLimit(StreamUsageEnum streamUsage, const Optional<DataModel::Nullable<uint16_t>> & videoStreamId,
const Optional<DataModel::Nullable<uint16_t>> & audioStreamId) override;
diff --git a/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp b/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp
index 9f795d9..056449e 100644
--- a/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp
+++ b/examples/all-clusters-app/all-clusters-common/src/push-av-stream-transport-delegate-impl.cpp
@@ -121,11 +121,6 @@
return Status::Success;
}
-bool PushAvStreamTransportManager::ValidateUrl(const std::string & url)
-{
- return true;
-}
-
Protocols::InteractionModel::Status PushAvStreamTransportManager::SelectVideoStream(StreamUsageEnum streamUsage,
uint16_t & videoStreamId)
{
diff --git a/examples/camera-app/linux/include/clusters/push-av-stream-transport/push-av-stream-manager.h b/examples/camera-app/linux/include/clusters/push-av-stream-transport/push-av-stream-manager.h
index 0c787d7..b8f3bc4 100644
--- a/examples/camera-app/linux/include/clusters/push-av-stream-transport/push-av-stream-manager.h
+++ b/examples/camera-app/linux/include/clusters/push-av-stream-transport/push-av-stream-manager.h
@@ -76,8 +76,6 @@
void SetTLSCerts(Tls::CertificateTable::BufferedClientCert & clientCertEntry,
Tls::CertificateTable::BufferedRootCert & rootCertEntry) override;
- bool ValidateUrl(const std::string & url) override;
-
bool ValidateStreamUsage(StreamUsageEnum streamUsage) override;
bool ValidateSegmentDuration(uint16_t segmentDuration, const Optional<DataModel::Nullable<uint16_t>> & videoStreamId) override;
diff --git a/examples/camera-app/linux/src/clusters/push-av-stream-transport/push-av-stream-manager.cpp b/examples/camera-app/linux/src/clusters/push-av-stream-transport/push-av-stream-manager.cpp
index 5f5feff..935efe7 100644
--- a/examples/camera-app/linux/src/clusters/push-av-stream-transport/push-av-stream-manager.cpp
+++ b/examples/camera-app/linux/src/clusters/push-av-stream-transport/push-av-stream-manager.cpp
@@ -361,24 +361,6 @@
return false;
}
-bool PushAvStreamTransportManager::ValidateUrl(const std::string & url)
-{
- const std::string https = "https://";
-
- // Check minimum length and https prefix
- if (url.size() <= https.size() || url.substr(0, https.size()) != https)
- {
- return false;
- }
-
- // Check for non-empty host
- size_t hostStart = https.size();
- size_t hostEnd = url.find('/', hostStart);
- std::string host = (hostEnd == std::string::npos) ? url.substr(hostStart) : url.substr(hostStart, hostEnd - hostStart);
-
- return !host.empty();
-}
-
Protocols::InteractionModel::Status PushAvStreamTransportManager::SelectVideoStream(StreamUsageEnum streamUsage,
uint16_t & videoStreamId)
{
diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h
index a16c2a4..58cc890 100644
--- a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h
+++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-delegate.h
@@ -130,14 +130,6 @@
const Optional<PushAvStreamTransport::Structs::TransportMotionTriggerTimeControlStruct::Type> & timeControl) = 0;
/**
- * @brief Validates the provided URL.
- *
- * @param url The URL to validate
- * @return true if URL is valid, false otherwise
- */
- virtual bool ValidateUrl(const std::string & url) = 0;
-
- /**
* @brief Validates the provided StreamUsage.
*
* @param streamUsage The StreamUsage to validate
diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.cpp b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.cpp
index 8ebc5c7..b2a2a53 100644
--- a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.cpp
+++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.cpp
@@ -232,6 +232,86 @@
}
}
+bool PushAvStreamTransportServerLogic::ValidateUrl(const std::string & url)
+{
+ const std::string https = "https://";
+
+ // Check minimum length and https prefix
+ if (url.size() <= https.size() || url.substr(0, https.size()) != https)
+ {
+ return false;
+ }
+
+ // Check that URL does not contain fragment character '#'
+ if (url.find('#') != std::string::npos)
+ {
+ ChipLogError(Camera, "URL contains fragment character '#'");
+ return false;
+ }
+
+ // Check that URL does not contain query character '?'
+ if (url.find('?') != std::string::npos)
+ {
+ ChipLogError(Camera, "URL contains query character '?'");
+ return false;
+ }
+
+ // Check that URL ends with a forward slash '/'
+ if (url.back() != '/')
+ {
+ ChipLogError(Camera, "URL does not end with '/'");
+ return false;
+ }
+
+ // Extract host part
+ size_t hostStart = https.size();
+ size_t hostEnd = url.find('/', hostStart);
+ // If no '/' is found after the scheme, the rest of the URL is the host.
+ // If a '/' is found, the host is the part between the scheme and the first '/'.
+ std::string host;
+ if (hostEnd == std::string::npos)
+ {
+ // This case handles URLs like "https://example.com" or "https://localhost"
+ // The host is the entire string after "https://"
+ host = url.substr(hostStart);
+ }
+ else
+ {
+ // This case handles URLs like "https://example.com/" or "https://example.com/path"
+ // The host is the part between "https://" and the first '/'
+ host = url.substr(hostStart, hostEnd - hostStart);
+ }
+ // Check for non-empty host. This check is now more robust.
+ if (host.empty())
+ {
+ ChipLogError(Camera, "URL does not contain a valid host.");
+ return false;
+ }
+ // Allow 'localhost' and 'localhost:<port>'
+ if (host == "localhost" || (host.length() > 10 && host.substr(0, 10) == "localhost:"))
+ {
+ // Additional check to ensure there's something after the colon for port
+ if (host == "localhost:" || (host.length() > 10 && host[10] == '\0'))
+ {
+ // This would be "localhost:" with nothing after, which is invalid
+ ChipLogError(Camera, "URL host '%s' is not valid. 'localhost:' must be followed by a port number.", host.c_str());
+ return false;
+ }
+ return true;
+ }
+ // Simplified host validation:
+ // A valid host should typically contain a dot (e.g., 'example.com').
+ // This is a basic check and not a comprehensive host/IP validation.
+ if (host.find('.') == std::string::npos)
+ {
+ ChipLogError(Camera, "URL host '%s' is not valid. Must be 'localhost', 'localhost:<port>', or contain a dot.",
+ host.c_str());
+ return false;
+ }
+
+ return true;
+}
+
CHIP_ERROR PushAvStreamTransportServerLogic::ScheduleTransportDeallocate(uint16_t connectionID, uint32_t timeoutSec)
{
uint32_t timeoutMs = timeoutSec * MILLISECOND_TICKS_PER_SECOND;
@@ -621,7 +701,7 @@
return std::nullopt;
}
- bool isValidUrl = mDelegate->ValidateUrl(std::string(transportOptions.url.data(), transportOptions.url.size()));
+ bool isValidUrl = ValidateUrl(std::string(transportOptions.url.data(), transportOptions.url.size()));
if (isValidUrl == false)
{
diff --git a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h
index 7161433..e6b4170 100644
--- a/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h
+++ b/src/app/clusters/push-av-stream-transport-server/push-av-stream-transport-logic.h
@@ -163,6 +163,14 @@
* @param timeoutSec timeout in seconds
*/
CHIP_ERROR ScheduleTransportDeallocate(uint16_t connectionID, uint32_t timeoutSec);
+
+ /**
+ * @brief Validates the provided URL.
+ *
+ * @param url The URL to validate
+ * @return true if URL is valid, false otherwise
+ */
+ bool ValidateUrl(const std::string & url);
};
} // namespace Clusters
diff --git a/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp
index b31dcb2..ff7c9ae 100644
--- a/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp
+++ b/src/app/clusters/push-av-stream-transport-server/tests/TestPushAVStreamTransportCluster.cpp
@@ -299,8 +299,6 @@
return Status::Success;
}
- bool ValidateUrl(const std::string & url) override { return true; }
-
Protocols::InteractionModel::Status SelectVideoStream(StreamUsageEnum streamUsage, uint16_t & videoStreamId) override
{
// TODO: Select and Assign videoStreamID from the allocated videoStreams
@@ -469,7 +467,7 @@
std::vector<TransportZoneOptionsDecodableStruct> mTransportZoneOptions;
TransportTriggerOptionsDecodableStruct triggerOptions;
- std::string url = "rtsp://192.168.1.100:554/stream";
+ std::string url = "https://192.168.1.100:554/stream/";
TransportOptionsDecodableStruct transportOptions;
uint8_t tlvBuffer[512];
@@ -598,7 +596,7 @@
std::vector<TransportZoneOptionsDecodableStruct> mTransportZoneOptions;
TransportTriggerOptionsDecodableStruct triggerOptions;
- std::string url = "rtsp://192.168.1.100:554/stream";
+ std::string url = "https://192.168.1.100:554/stream/";
TransportOptionsDecodableStruct transportOptions;
uint8_t tlvBuffer[512];
@@ -756,7 +754,7 @@
EXPECT_EQ(respTransportOptions.audioStreamID, 2);
EXPECT_EQ(respTransportOptions.TLSEndpointID, 1);
std::string respUrlStr(respTransportOptions.url.data(), respTransportOptions.url.size());
- EXPECT_EQ(respUrlStr, "rtsp://192.168.1.100:554/stream");
+ EXPECT_EQ(respUrlStr, "https://192.168.1.100:554/stream/");
Structs::TransportTriggerOptionsStruct::DecodableType respTriggerOptions = respTransportOptions.triggerOptions;
EXPECT_EQ(respTriggerOptions.triggerType, TransportTriggerTypeEnum::kMotion);
@@ -892,7 +890,7 @@
EXPECT_EQ(readTransportOptions.TLSEndpointID, 1);
std::string urlStr(readTransportOptions.url.data(), readTransportOptions.url.size());
- EXPECT_EQ(urlStr, "rtsp://192.168.1.100:554/stream");
+ EXPECT_EQ(urlStr, "https://192.168.1.100:554/stream/");
Structs::TransportTriggerOptionsStruct::DecodableType readTriggerOptions = readTransportOptions.triggerOptions;
EXPECT_EQ(readTriggerOptions.triggerType, TransportTriggerTypeEnum::kMotion);
@@ -985,7 +983,7 @@
std::vector<TransportZoneOptionsDecodableStruct> mTransportZoneOptions;
TransportTriggerOptionsDecodableStruct triggerOptions;
- std::string url = "rtsp://192.168.1.100:554/stream";
+ std::string url = "https://192.168.1.100:554/stream/";
TransportOptionsDecodableStruct transportOptions;
uint8_t tlvBuffer[512];
@@ -1162,7 +1160,7 @@
transportOptions.videoStreamID.SetValue(11);
transportOptions.audioStreamID.SetValue(22);
transportOptions.TLSEndpointID = 1;
- url = "rtsp://192.168.1.100:554/modify-stream";
+ url = "https://192.168.1.100:554/modify-stream/";
transportOptions.url = Span(url.data(), url.size());
transportOptions.triggerOptions = triggerOptions;
transportOptions.containerOptions = containerOptions;
@@ -1245,7 +1243,7 @@
EXPECT_EQ(findTransportOptions.TLSEndpointID, 1);
std::string findUrlStr(findTransportOptions.url.data(), findTransportOptions.url.size());
- EXPECT_EQ(findUrlStr, "rtsp://192.168.1.100:554/modify-stream");
+ EXPECT_EQ(findUrlStr, "https://192.168.1.100:554/modify-stream/");
Structs::TransportTriggerOptionsStruct::DecodableType findTriggerOptions = findTransportOptions.triggerOptions;
EXPECT_EQ(findTriggerOptions.triggerType, TransportTriggerTypeEnum::kMotion);
@@ -1320,7 +1318,7 @@
std::vector<TransportZoneOptionsDecodableStruct> mTransportZoneOptions;
TransportTriggerOptionsDecodableStruct triggerOptions;
- std::string url = "rtsp://192.168.1.100:554/stream";
+ std::string url = "https://192.168.1.100:554/stream/";
TransportOptionsDecodableStruct transportOptions;
uint8_t tlvBuffer[512];
diff --git a/src/python_testing/TC_PAVSTI_1_1.py b/src/python_testing/TC_PAVSTI_1_1.py
index 1dbefbd..9a5b1ee 100644
--- a/src/python_testing/TC_PAVSTI_1_1.py
+++ b/src/python_testing/TC_PAVSTI_1_1.py
@@ -286,7 +286,7 @@
"videoStreamID": videoStreamId,
"audioStreamID": audioStreamId,
"TLSEndpointID": tlsEndpointId,
- "url": f"https://{self.host_ip}:1234/streams/{uploadStreamId}",
+ "url": f"https://{self.host_ip}:1234/streams/{uploadStreamId}/",
"triggerOptions": {"triggerType": pushavCluster.Enums.TransportTriggerTypeEnum.kCommand, "maxPreRollLen": 10},
"ingestMethod": pushavCluster.Enums.IngestMethodsEnum.kCMAFIngest,
"containerFormat": pushavCluster.Enums.ContainerFormatEnum.kCmaf,
diff --git a/src/python_testing/TC_PAVSTI_1_2.py b/src/python_testing/TC_PAVSTI_1_2.py
index 5cd591a..15042c8 100644
--- a/src/python_testing/TC_PAVSTI_1_2.py
+++ b/src/python_testing/TC_PAVSTI_1_2.py
@@ -243,7 +243,7 @@
"videoStreamID": videoStreamId,
"audioStreamID": audioStreamId,
"TLSEndpointID": tlsEndpointId,
- "url": f"https://{self.host_ip}:1234/streams/{uploadStreamId}",
+ "url": f"https://{self.host_ip}:1234/streams/{uploadStreamId}/",
"triggerOptions": {"triggerType": pushavCluster.Enums.TransportTriggerTypeEnum.kContinuous},
"ingestMethod": pushavCluster.Enums.IngestMethodsEnum.kCMAFIngest,
"containerFormat": pushavCluster.Enums.ContainerFormatEnum.kCmaf,
diff --git a/src/python_testing/TC_PAVSTTestBase.py b/src/python_testing/TC_PAVSTTestBase.py
index dde9b93..24374c8 100644
--- a/src/python_testing/TC_PAVSTTestBase.py
+++ b/src/python_testing/TC_PAVSTTestBase.py
@@ -187,7 +187,7 @@
async def allocate_one_pushav_transport(self, endpoint, triggerType=Clusters.PushAvStreamTransport.Enums.TransportTriggerTypeEnum.kContinuous,
trigger_Options=None, ingestMethod=Clusters.PushAvStreamTransport.Enums.IngestMethodsEnum.kCMAFIngest,
- url="https://localhost:1234/streams/1", stream_Usage=None, container_Options=None,
+ url="https://localhost:1234/streams/1/", stream_Usage=None, container_Options=None,
videoStream_ID=None, audioStream_ID=None, expected_cluster_status=None, tlsEndPoint=1, expiryTime=10):
endpoint = self.get_endpoint(default=1)
cluster = Clusters.PushAvStreamTransport
@@ -233,7 +233,7 @@
containerOptions = {
"containerType": cluster.Enums.ContainerFormatEnum.kCmaf,
"CMAFContainerOptions": {"CMAFInterface": cluster.Enums.CMAFInterfaceEnum.kInterface1, "chunkDuration": 4, "segmentDuration": 4000,
- "sessionGroup": 3, "trackName": " "},
+ "sessionGroup": 3, "trackName": "media"},
}
if (container_Options is not None):
diff --git a/src/python_testing/TC_PAVST_2_10.py b/src/python_testing/TC_PAVST_2_10.py
new file mode 100644
index 0000000..29147bc
--- /dev/null
+++ b/src/python_testing/TC_PAVST_2_10.py
@@ -0,0 +1,169 @@
+#
+# Copyright (c) 2025 Project CHIP Authors
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
+# for details about the block below.
+#
+# === BEGIN CI TEST ARGUMENTS ===
+# test-runner-runs:
+# run1:
+# app: ${CAMERA_APP}
+# app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# script-args: >
+# --storage-path admin_storage.json
+# --string-arg th_server_app_path:${PUSH_AV_SERVER}
+# --string-arg host_ip:localhost
+# --commissioning-method on-network
+# --discriminator 1234
+# --passcode 20202021
+# --PICS src/app/tests/suites/certification/ci-pics-values
+# --trace-to json:${TRACE_TEST_JSON}.json
+# --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+# --endpoint 1
+# factory-reset: true
+# quiet: true
+# === END CI TEST ARGUMENTS ===
+
+import logging
+
+from mobly import asserts
+from TC_PAVSTI_Utils import PAVSTIUtils, PushAvServerProcess
+from TC_PAVSTTestBase import PAVSTTestBase
+
+import matter.clusters as Clusters
+from matter.interaction_model import InteractionModelError
+from matter.testing.matter_testing import (MatterBaseTest, TestStep, async_test_body, default_matter_test_main, has_cluster,
+ run_if_endpoint_matches)
+
+logger = logging.getLogger(__name__)
+
+
+class TC_PAVST_2_10(MatterBaseTest, PAVSTTestBase, PAVSTIUtils):
+ def desc_TC_PAVST_2_10(self) -> str:
+ return "[TC-PAVST-2.10] Validate URL validation in clip upload"
+
+ def pics_TC_PAVST_2_10(self):
+ return ["PAVST.S", "AVSM.S"]
+
+ @async_test_body
+ async def setup_class(self):
+ th_server_app = self.user_params.get("th_server_app_path", None)
+ self.server = PushAvServerProcess(server_path=th_server_app)
+ self.server.start(
+ expected_output="Running on https://0.0.0.0:1234",
+ timeout=30,
+ )
+ super().setup_class()
+
+ def teardown_class(self):
+ if self.server is not None:
+ self.server.terminate()
+ super().teardown_class()
+
+ def steps_TC_PAVST_2_10(self) -> list[TestStep]:
+ return [
+ TestStep("precondition", "Commissioning, already done", is_commissioning=True),
+ TestStep(1, "TH Reads CurrentConnections attribute from PushAV Stream Transport Cluster on DUT",
+ "Verify the number of PushAV Connections is 0. If not 0, deallocate any existing connections."),
+ TestStep(2, "TH Reads SupportedFormats attribute from PushAV Stream Transport Cluster on DUT",
+ "Store the SupportedFormats as aSupportedFormats."),
+ TestStep(3, "TH Reads AllocatedVideoStreams attribute from CameraAVStreamManagement Cluster on DUT",
+ "Store as aAllocatedVideoStreams."),
+ TestStep(4, "TH Reads AllocatedAudioStreams attribute from CameraAVStreamManagement Cluster on DUT",
+ "Store as aAllocatedAudioStreams."),
+ TestStep(5, "TH sends AllocatePushTransport command with URL using non‑https scheme",
+ "DUT should respond with Status Code InvalidURL."),
+ TestStep(6, "TH sends AllocatePushTransport command with URL containing a fragment (#)",
+ "DUT should respond with Status Code InvalidURL."),
+ TestStep(7, "TH sends AllocatePushTransport command with URL containing a query (?)",
+ "DUT should respond with Status Code InvalidURL."),
+ TestStep(8, "TH sends AllocatePushTransport command with URL not ending with '/'",
+ "DUT should respond with Status Code InvalidURL."),
+ TestStep(9, "TH sends AllocatePushTransport command with URL missing host",
+ "DUT should respond with Status Code InvalidURL."),
+ ]
+
+ @run_if_endpoint_matches(has_cluster(Clusters.PushAvStreamTransport))
+ async def test_TC_PAVST_2_10(self):
+ endpoint = self.get_endpoint(default=1)
+ self.endpoint = endpoint
+ self.node_id = self.dut_node_id
+ pvcluster = Clusters.PushAvStreamTransport
+ pvattr = Clusters.PushAvStreamTransport.Attributes
+
+ # Precondition
+ self.step("precondition")
+ host_ip = self.user_params.get("host_ip", None)
+ tlsEndpointId, host_ip = await self.precondition_provision_tls_endpoint(
+ endpoint=endpoint, server=self.server, host_ip=host_ip)
+
+ # Reads CurrentConnections attribute (step 1)
+ self.step(1)
+ transport_configs = await self.read_single_attribute_check_success(
+ endpoint=endpoint, cluster=pvcluster, attribute=pvattr.CurrentConnections)
+ for cfg in transport_configs:
+ if cfg.ConnectionID != 0:
+ try:
+ await self.send_single_cmd(
+ cmd=pvcluster.Commands.DeallocatePushTransport(ConnectionID=cfg.ConnectionID),
+ endpoint=endpoint)
+ except InteractionModelError as e:
+ logging.warning(f"Failed to deallocate connection {cfg.ConnectionID} during cleanup: {e}")
+
+ # Read supported formats (step 2)
+ self.step(2)
+ aSupportedFormats = await self.read_single_attribute_check_success(
+ endpoint=endpoint, cluster=pvcluster, attribute=pvattr.SupportedFormats)
+ logger.info(f"aSupportedFormats={aSupportedFormats}")
+
+ # Read allocated video streams (step 3)
+ self.step(3)
+ aAllocatedVideoStreams = await self.allocate_one_video_stream()
+ asserts.assert_greater_equal(
+ len(aAllocatedVideoStreams),
+ 1,
+ "AllocatedVideoStreams must not be empty",
+ )
+
+ # Read allocated audio streams (step 4)
+ self.step(4)
+ aAllocatedAudioStreams = await self.allocate_one_audio_stream()
+ asserts.assert_greater_equal(
+ len(aAllocatedAudioStreams),
+ 1,
+ "AllocatedAudioStreams must not be empty",
+ )
+
+ # Define invalid URL cases
+ stream_id = self.server.create_stream()
+ invalid_cases = [
+ ("non‑https scheme", f"http://{host_ip}:1234/streams/{stream_id}/"),
+ ("fragment", f"https://{host_ip}:1234/streams/{stream_id}#/frag"),
+ ("query", f"https://{host_ip}:1234/streams/{stream_id}?bad=query"),
+ ("no trailing slash", f"https://{host_ip}:1234/streams/{stream_id}"),
+ ("missing host", f"https:///streams/{stream_id}/"),
+ ]
+
+ for idx, (desc, url) in enumerate(invalid_cases, start=5):
+ self.step(idx)
+ status = await self.allocate_one_pushav_transport(
+ endpoint, tlsEndPoint=tlsEndpointId, url=url, expected_cluster_status=pvcluster.Enums.StatusCodeEnum.kInvalidURL, expiryTime=30)
+ asserts.assert_equal(status, pvcluster.Enums.StatusCodeEnum.kInvalidURL,
+ f"Push AV Transport should return InvalidURL for {desc}")
+
+
+if __name__ == "__main__":
+ default_matter_test_main()
diff --git a/src/python_testing/TC_PAVST_2_2.py b/src/python_testing/TC_PAVST_2_2.py
index 4715bea..266aaac 100644
--- a/src/python_testing/TC_PAVST_2_2.py
+++ b/src/python_testing/TC_PAVST_2_2.py
@@ -139,7 +139,7 @@
)
self.step(5)
- status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
diff --git a/src/python_testing/TC_PAVST_2_3.py b/src/python_testing/TC_PAVST_2_3.py
index 7d0bac4..4646bad 100644
--- a/src/python_testing/TC_PAVST_2_3.py
+++ b/src/python_testing/TC_PAVST_2_3.py
@@ -187,7 +187,7 @@
"videoStreamID": NullValue,
"audioStreamID": NullValue,
"TLSEndpointID": tlsEndpointId,
- "url": f"https://{host_ip}:1234/streams/1",
+ "url": f"https://{host_ip}:1234/streams/1/",
"triggerOptions": {"triggerType": 2},
"ingestMethod": 0,
"containerOptions": {
@@ -219,7 +219,7 @@
)
self.step(6)
- status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
@@ -261,7 +261,7 @@
"videoStreamID": 1,
"audioStreamID": 1,
"TLSEndpointID": 5,
- "url": f"https://{host_ip}:1234/streams/1",
+ "url": f"https://{host_ip}:1234/streams/1/",
"triggerOptions": {"triggerType": 2},
"ingestMethod": 0,
"containerOptions": {
@@ -278,19 +278,19 @@
self.step(12)
status = await self.allocate_one_pushav_transport(endpoint, ingestMethod=pvcluster.Enums.IngestMethodsEnum.kUnknownEnumValue,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.ConstraintError,
"DUT must respond with Status Code ConstraintError.")
self.step(13)
status = await self.allocate_one_pushav_transport(endpoint, expected_cluster_status=pvcluster.Enums.StatusCodeEnum.kInvalidURL,
- tlsEndPoint=tlsEndpointId, url=f"https:/{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https:/{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, pvcluster.Enums.StatusCodeEnum.kInvalidURL,
"DUT must respond with Status Code InvalidURL.")
self.step(14)
status = await self.allocate_one_pushav_transport(endpoint, triggerType=pvcluster.Enums.TransportTriggerTypeEnum.kUnknownEnumValue,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.ConstraintError,
"DUT must respond with Status Code ConstraintError.")
@@ -302,7 +302,7 @@
"maxPreRollLen": 4000,
"motionZones": zoneList,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.Success, "DUT should respond with Status Code Success with a Null Zone.")
except InteractionModelError as e:
asserts.assert_fail(f"Unexpected error when setting a Zone that is Null (meaning all Zones). Error received {e.status}")
@@ -315,7 +315,7 @@
"maxPreRollLen": 4000,
"motionZones": zoneList,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.AlreadyExists,
"DUT should respond with Status Code AlreadyExists with a Duplicate Zone.")
@@ -327,7 +327,7 @@
"maxPreRollLen": 4000,
"motionZones": zoneList,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.AlreadyExists,
"DUT should respond with Status Code AlreadyExists with Duplicate Null Zones.")
@@ -339,7 +339,7 @@
"motionZones": zoneList,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, expected_cluster_status=pvcluster.Enums.StatusCodeEnum.kInvalidZone,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, pvcluster.Enums.StatusCodeEnum.kInvalidZone,
"DUT must responds with Status Code InvalidZone.")
except InteractionModelError as e:
@@ -349,14 +349,14 @@
self.step(19)
status = await self.allocate_one_pushav_transport(endpoint, videoStream_ID=-1,
expected_cluster_status=pvcluster.Enums.StatusCodeEnum.kInvalidStream,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, pvcluster.Enums.StatusCodeEnum.kInvalidStream,
"DUT must responds with Status Code InvalidStream.")
self.step(20)
status = await self.allocate_one_pushav_transport(endpoint, audioStream_ID=-1,
expected_cluster_status=pvcluster.Enums.StatusCodeEnum.kInvalidStream,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, pvcluster.Enums.StatusCodeEnum.kInvalidStream,
"DUT must responds with Status Code InvalidStream.")
@@ -390,7 +390,7 @@
self.step(22)
status = await self.allocate_one_pushav_transport(endpoint, videoStream_ID=Nullable(), audioStream_ID=Nullable(),
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.Success,
"DUT must responds with Status Code Success.")
@@ -405,7 +405,7 @@
"motionZones": zoneList,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.DynamicConstraintError,
"DUT must respond with Status code DynamicConstraintError")
@@ -415,7 +415,7 @@
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1},
"motionSensitivity": 3,
"motionZones": zoneList}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.InvalidCommand,
"DUT must responds with Status Code InvalidCommand.")
@@ -425,7 +425,7 @@
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1},
"motionSensitivity": 11,
"motionZones": zoneList}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.InvalidCommand,
"DUT must responds with Status Code InvalidCommand.")
@@ -434,7 +434,7 @@
triggerOptions = {"triggerType": pvcluster.Enums.TransportTriggerTypeEnum.kMotion,
"motionZones": zoneList,
"motionSensitivity": 3}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.InvalidCommand,
"DUT must responds with Status Code InvalidCommand.")
@@ -444,7 +444,7 @@
"motionZones": zoneList,
"motionSensitivity": 3,
"motionTimeControl": {"initialDuration": 0, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.ConstraintError,
"DUT must responds with Status code ConstraintError")
@@ -454,7 +454,7 @@
"motionZones": zoneList,
"motionSensitivity": 3,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 0, "blindDuration": 1}}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.ConstraintError,
"DUT must responds with Status code ConstraintError")
@@ -471,7 +471,7 @@
"motionZones": zoneList,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.Success,
"DUT must responds with Status Code Success.")
current_connections = await self.read_single_attribute_check_success(
@@ -491,19 +491,19 @@
"maxPreRollLen": 4000,
"motionZones": zoneList,
"motionTimeControl": {"initialDuration": 1, "augmentationDuration": 1, "maxDuration": 1, "blindDuration": 1}}
- status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.Success,
"DUT must responds with Status Code Success.")
self.step(31)
status = await self.allocate_one_pushav_transport(endpoint, stream_Usage=Clusters.Globals.Enums.StreamUsageEnum.kUnknownEnumValue,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.ConstraintError,
"DUT must responds with Status code ConstraintError")
self.step(32)
containerOptions = {"containerType": pvcluster.Enums.ContainerFormatEnum.kCmaf}
- status = await self.allocate_one_pushav_transport(endpoint, container_Options=containerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, container_Options=containerOptions, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, Status.InvalidCommand,
"DUT must responds with Status code InvalidCommand")
@@ -511,18 +511,18 @@
containerOptions = {
"containerType": pvcluster.Enums.ContainerFormatEnum.kCmaf,
"CMAFContainerOptions": {"CMAFInterface": pvcluster.Enums.CMAFInterfaceEnum.kInterface1, "chunkDuration": 4, "segmentDuration": 6000,
- "sessionGroup": 3, "trackName": " "},
+ "sessionGroup": 3, "trackName": "media"},
}
status = await self.allocate_one_pushav_transport(endpoint, expected_cluster_status=pvcluster.Enums.StatusCodeEnum.kInvalidOptions,
stream_Usage=Clusters.Globals.Enums.StreamUsageEnum.kRecording, tlsEndPoint=tlsEndpointId,
- container_Options=containerOptions, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ container_Options=containerOptions, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, pvcluster.Enums.StatusCodeEnum.kInvalidOptions,
"DUT must responds with Status Code InvalidOptions.")
self.step(34)
status = await self.allocate_one_pushav_transport(endpoint, expected_cluster_status=pvcluster.Enums.StatusCodeEnum.kInvalidStreamUsage,
stream_Usage=Clusters.Globals.Enums.StreamUsageEnum.kInternal,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(status, pvcluster.Enums.StatusCodeEnum.kInvalidStreamUsage,
"DUT must responds with Status code InvalidStreamUsage")
diff --git a/src/python_testing/TC_PAVST_2_4.py b/src/python_testing/TC_PAVST_2_4.py
index a88a77a..1c80e7e 100644
--- a/src/python_testing/TC_PAVST_2_4.py
+++ b/src/python_testing/TC_PAVST_2_4.py
@@ -147,7 +147,7 @@
"AllocatedAudioStreams must not be empty",
)
- status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
diff --git a/src/python_testing/TC_PAVST_2_5.py b/src/python_testing/TC_PAVST_2_5.py
index 18a46d8..150ecf7 100644
--- a/src/python_testing/TC_PAVST_2_5.py
+++ b/src/python_testing/TC_PAVST_2_5.py
@@ -146,7 +146,7 @@
"AllocatedAudioStreams must not be empty",
)
- status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
diff --git a/src/python_testing/TC_PAVST_2_6.py b/src/python_testing/TC_PAVST_2_6.py
index fd627f9..51a5e22 100644
--- a/src/python_testing/TC_PAVST_2_6.py
+++ b/src/python_testing/TC_PAVST_2_6.py
@@ -181,7 +181,7 @@
"AllocatedAudioStreams must not be empty",
)
- status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
diff --git a/src/python_testing/TC_PAVST_2_7.py b/src/python_testing/TC_PAVST_2_7.py
index 8cf120a..d21751a 100644
--- a/src/python_testing/TC_PAVST_2_7.py
+++ b/src/python_testing/TC_PAVST_2_7.py
@@ -188,7 +188,7 @@
)
status = await self.allocate_one_pushav_transport(endpoint, triggerType=pvcluster.Enums.TransportTriggerTypeEnum.kContinuous,
- tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
@@ -303,7 +303,7 @@
"maxPreRollLen": 4000}
status = await self.allocate_one_pushav_transport(endpoint, trigger_Options=triggerOptions, tlsEndPoint=tlsEndpointId,
- url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
diff --git a/src/python_testing/TC_PAVST_2_8.py b/src/python_testing/TC_PAVST_2_8.py
index 19358cd..ca4f465 100644
--- a/src/python_testing/TC_PAVST_2_8.py
+++ b/src/python_testing/TC_PAVST_2_8.py
@@ -147,7 +147,7 @@
"AllocatedAudioStreams must not be empty",
)
- status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}")
+ status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/")
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)
diff --git a/src/python_testing/TC_PAVST_2_9.py b/src/python_testing/TC_PAVST_2_9.py
index 8bcbfd9..a3c8f18 100644
--- a/src/python_testing/TC_PAVST_2_9.py
+++ b/src/python_testing/TC_PAVST_2_9.py
@@ -147,7 +147,7 @@
)
self.step(5)
- status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}", expiryTime=5)
+ status = await self.allocate_one_pushav_transport(endpoint, tlsEndPoint=tlsEndpointId, url=f"https://{host_ip}:1234/streams/{uploadStreamId}/", expiryTime=5)
asserts.assert_equal(
status, Status.Success, "Push AV Transport should be allocated successfully"
)