[WebRTCP] Implement test case for TC_WEBRTCP_2_12 & TC_WEBRTCP_2_16 (#41214)

* Implement test case for TC_WEBRTCP_2_12

* Address gemini review comments

* Fix python warnings

* Remove unused import

* Use lower case for varibles
diff --git a/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp b/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp
index 80cb982..692902a 100644
--- a/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp
+++ b/examples/camera-app/linux/src/clusters/webrtc-provider/webrtc-provider-manager.cpp
@@ -32,6 +32,13 @@
 using namespace chip::app::Clusters;
 using namespace chip::app::Clusters::WebRTCTransportProvider;
 
+namespace {
+
+// Constants
+constexpr uint16_t kMaxConcurrentWebRTCSessions = 5;
+
+} // namespace
+
 void WebRTCProviderManager::SetCameraDevice(CameraDeviceInterface * aCameraDevice)
 {
     mCameraDevice = aCameraDevice;
@@ -134,6 +141,23 @@
     }
 
     transport->SetRequestArgs(requestArgs);
+
+    // Check resource availability before proceeding
+    // If we cannot allocate resources, send End command with OutOfResources reason
+    if (mWebrtcTransportMap.size() > kMaxConcurrentWebRTCSessions)
+    {
+        ChipLogProgress(Camera, "Resource exhaustion detected: maximum WebRTC sessions (%u)", kMaxConcurrentWebRTCSessions);
+
+        transport->SetCommandType(WebrtcTransport::CommandType::kEnd);
+        transport->MoveToState(WebrtcTransport::State::SendingEnd);
+
+        // The resource exhaustion happens internally in the DUT, but it still creates a session
+        // and then sends an End command with OutOfResources reason.
+        ScheduleEndSend(args.sessionId);
+
+        return CHIP_NO_ERROR;
+    }
+
     transport->Start();
     transport->AddAudioTrack();
     transport->AddVideoTrack();
@@ -282,6 +306,15 @@
             [this](bool connected, const uint16_t sessionId) { this->OnConnectionStateChanged(connected, sessionId); });
     }
 
+    // Check resource availability before proceeding
+    // If we cannot allocate resources, respond with a response status of RESOURCE_EXHAUSTED
+    if (mWebrtcTransportMap.size() > kMaxConcurrentWebRTCSessions)
+    {
+        ChipLogProgress(Camera, "Resource exhaustion detected in ProvideOffer: maximum WebRTC sessions (%u)",
+                        kMaxConcurrentWebRTCSessions);
+        return CHIP_IM_GLOBAL_STATUS(ResourceExhausted);
+    }
+
     transport->SetRequestArgs(requestArgs);
     transport->Start();
     auto peerConnection  = transport->GetPeerConnection();
diff --git a/src/python_testing/TC_WEBRTCP_2_12.py b/src/python_testing/TC_WEBRTCP_2_12.py
new file mode 100644
index 0000000..f6eff41
--- /dev/null
+++ b/src/python_testing/TC_WEBRTCP_2_12.py
@@ -0,0 +1,186 @@
+#
+#  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: >
+#       --PICS src/app/tests/suites/certification/ci-pics-values
+#       --storage-path admin_storage.json
+#       --commissioning-method on-network
+#       --discriminator 1234
+#       --passcode 20202021
+#       --trace-to json:${TRACE_TEST_JSON}.json
+#       --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+#     factory-reset: true
+#     quiet: true
+# === END CI TEST ARGUMENTS ===
+#
+
+import logging
+
+from mobly import asserts
+from TC_WEBRTCPTestBase import WEBRTCPTestBase
+
+import matter.clusters as Clusters
+from matter import ChipDeviceCtrl
+from matter.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from matter.webrtc import LibdatachannelPeerConnection, WebRTCManager
+
+logger = logging.getLogger(__name__)
+
+
+class TC_WEBRTCP_2_12(MatterBaseTest, WEBRTCPTestBase):
+    def desc_TC_WEBRTCP_2_12(self) -> str:
+        """Returns a description of this test"""
+        return "[TC-WEBRTCP-2.12] Validate SolicitOffer resource exhaustion"
+
+    def steps_TC_WEBRTCP_2_12(self) -> list[TestStep]:
+        steps = [
+            TestStep(1, "TH allocates both Audio and Video streams via AudioStreamAllocate and VideoStreamAllocate commands to CameraAVStreamManagement",
+                     "DUT responds with success and provides stream IDs"),
+            TestStep(2, "TH sends multiple SolicitOffer commands to exhaust the DUT's capacity for WebRTC sessions (DUT-specific limit)",
+                     "DUT successfully creates sessions until capacity is reached"),
+            TestStep(3, "TH sends an additional SolicitOffer command when DUT capacity is exhausted",
+                     "DUT responds with SolicitOfferResponse containing allocated WebRTCSessionID"),
+            TestStep(4, "TH waits for DUT to send End command with reason OutOfResources",
+                     "DUT sends End command with reason OutOfResources (value 0x08)")
+        ]
+        return steps
+
+    def pics_TC_WEBRTCP_2_12(self) -> list[str]:
+        pics = [
+            "WEBRTCP.S",
+            "AVSM.S",
+        ]
+        return pics
+
+    @async_test_body
+    async def test_TC_WEBRTCP_2_12(self):
+        """
+        Executes the test steps for validating SolicitOffer resource exhaustion.
+        """
+
+        endpoint = self.get_endpoint(default=1)
+
+        self.step(1)
+        # Allocate Audio and Video streams
+        audio_stream_id = await self.allocate_one_audio_stream()
+        video_stream_id = await self.allocate_one_video_stream()
+
+        # Validate that the streams were allocated successfully
+        await self.validate_allocated_audio_stream(audio_stream_id)
+        await self.validate_allocated_video_stream(video_stream_id)
+
+        # Create WebRTC manager and peer for receiving End commands
+        webrtc_manager = WebRTCManager(event_loop=self.event_loop)
+        webrtc_peer: LibdatachannelPeerConnection = webrtc_manager.create_peer(
+            node_id=self.dut_node_id, fabric_index=self.default_controller.GetFabricIndexInternal(), endpoint=endpoint
+        )
+
+        self.step(2)
+        # Send multiple SolicitOffer commands to exhaust the DUT's capacity
+        logger.info("Starting to send SolicitOffer commands to exhaust DUT capacity")
+
+        # Get the maximum concurrent WebRTC sessions from user or use default for CI
+        prompt_msg = (
+            "\nPlease enter the maximum number of concurrent WebRTC sessions supported by the DUT:\n"
+            "This value is DUT-specific and should be obtained from the DUT documentation or specifications.\n"
+            "Enter the number (e.g., 5): "
+        )
+
+        if self.is_pics_sdk_ci_only:
+            # Use default value for CI testing
+            max_attempts = 5
+        else:
+            user_input = self.wait_for_user_input(prompt_msg)
+            try:
+                max_attempts = int(user_input.strip())
+                asserts.assert_true(max_attempts > 0, "Maximum concurrent sessions must be greater than 0")
+                logger.info(f"Using user-specified max_attempts={max_attempts}")
+            except ValueError:
+                asserts.fail(f"Invalid input '{user_input}'. Please enter a valid number.")
+
+        # Try to allocate multiple sessions to reach the DUT's capacity limit
+
+        for attempt in range(max_attempts):
+            logger.info(f"Attempt {attempt + 1}: Sending SolicitOffer command")
+            resp: Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse = await webrtc_peer.send_command(
+                cmd=Clusters.WebRTCTransportProvider.Commands.SolicitOffer(
+                    streamUsage=Clusters.Objects.Globals.Enums.StreamUsageEnum.kLiveView,
+                    videoStreamID=video_stream_id,
+                    audioStreamID=audio_stream_id,
+                    originatingEndpointID=1,
+                ),
+                endpoint=endpoint,
+                payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD,
+            )
+            asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse,
+                                 "Incorrect response type")
+            session_id = resp.webRTCSessionID
+            asserts.assert_true(session_id >= 0, f"Invalid session ID: {session_id}")
+            logger.info(f"Created session {session_id} in attempt {attempt + 1}")
+            webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+        self.step(3)
+        # Send an additional SolicitOffer command when DUT capacity is exhausted
+        logger.info("Sending additional SolicitOffer command to trigger resource exhaustion")
+
+        # The resource exhaustion happens internally in the DUT, but it still creates a session
+        # and then sends an End command with OutOfResources reason.
+        resp: Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse = await webrtc_peer.send_command(
+            cmd=Clusters.WebRTCTransportProvider.Commands.SolicitOffer(
+                streamUsage=Clusters.Objects.Globals.Enums.StreamUsageEnum.kLiveView,
+                videoStreamID=video_stream_id,
+                audioStreamID=audio_stream_id,
+                originatingEndpointID=1,
+            ),
+            endpoint=endpoint,
+            payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.SolicitOfferResponse,
+                             "Incorrect response type")
+        session_id = resp.webRTCSessionID
+        asserts.assert_true(session_id >= 0, f"Invalid session ID: {session_id}")
+        logger.info(f"DUT allocated session {session_id} despite resource exhaustion")
+        webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+        self.step(4)
+        # Wait for DUT to send End command with reason OutOfResources
+        logger.info("Waiting for DUT to send End command with OutOfResources reason")
+
+        # Wait for the End command from the DUT
+        end_sessionId, reason = await webrtc_peer.get_remote_end()
+
+        # Verify the End command has OutOfResources reason (value 0x08)
+        kOutOfResourcesReason = 0x08  # WebRTCEndReasonEnum.kOutOfResources
+        asserts.assert_equal(reason, kOutOfResourcesReason,
+                             f"Expected OutOfResources reason ({kOutOfResourcesReason}), got {reason}")
+
+        logger.info(f"Successfully received End command for session {end_sessionId} with OutOfResources reason {reason}")
+
+        await webrtc_manager.close_all()
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_WEBRTCP_2_13.py b/src/python_testing/TC_WEBRTCP_2_13.py
index f0ffa2c..3db4ed8 100644
--- a/src/python_testing/TC_WEBRTCP_2_13.py
+++ b/src/python_testing/TC_WEBRTCP_2_13.py
@@ -91,12 +91,12 @@
 
         self.step(1)
         # Allocate both Audio and Video streams
-        audioStreamID = await self.allocate_one_audio_stream()
-        videoStreamID = await self.allocate_one_video_stream()
+        audio_stream_id = await self.allocate_one_audio_stream()
+        video_stream_id = await self.allocate_one_video_stream()
 
         # Validate that the streams were allocated successfully
-        await self.validate_allocated_audio_stream(audioStreamID)
-        await self.validate_allocated_video_stream(videoStreamID)
+        await self.validate_allocated_audio_stream(audio_stream_id)
+        await self.validate_allocated_video_stream(video_stream_id)
 
         self.step(2)
         # For CI: Use app pipe to simulate physical privacy switch being turned on
@@ -140,8 +140,8 @@
             sdp=sdp_offer,
             streamUsage=3,
             originatingEndpointID=endpoint,
-            videoStreamID=videoStreamID,
-            audioStreamID=audioStreamID
+            videoStreamID=video_stream_id,
+            audioStreamID=audio_stream_id
         )
         try:
             await self.send_single_cmd(cmd=cmd, endpoint=endpoint, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
diff --git a/src/python_testing/TC_WEBRTCP_2_16.py b/src/python_testing/TC_WEBRTCP_2_16.py
new file mode 100644
index 0000000..febde32
--- /dev/null
+++ b/src/python_testing/TC_WEBRTCP_2_16.py
@@ -0,0 +1,180 @@
+#
+#  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: >
+#       --PICS src/app/tests/suites/certification/ci-pics-values
+#       --storage-path admin_storage.json
+#       --commissioning-method on-network
+#       --discriminator 1234
+#       --passcode 20202021
+#       --trace-to json:${TRACE_TEST_JSON}.json
+#       --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+#     factory-reset: true
+#     quiet: true
+# === END CI TEST ARGUMENTS ===
+#
+
+import logging
+
+from mobly import asserts
+from TC_WEBRTCPTestBase import WEBRTCPTestBase
+
+import matter.clusters as Clusters
+from matter import ChipDeviceCtrl
+from matter.clusters.Types import NullValue
+from matter.interaction_model import InteractionModelError, Status
+from matter.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+
+logger = logging.getLogger(__name__)
+
+
+class TC_WEBRTCP_2_16(MatterBaseTest, WEBRTCPTestBase):
+    def desc_TC_WEBRTCP_2_16(self) -> str:
+        """Returns a description of this test"""
+        return "[TC-WEBRTCP-2.16] Validate ProvideOffer resource exhaustion - PROVISIONAL"
+
+    def steps_TC_WEBRTCP_2_16(self) -> list[TestStep]:
+        steps = [
+            TestStep(1, "TH allocates both Audio and Video streams via AudioStreamAllocate and VideoStreamAllocate commands to CameraAVStreamManagement",
+                     "DUT responds with success and provides stream IDs"),
+            TestStep(2, "TH sends multiple ProvideOffer commands to exhaust DUT's session capacity (DUT-specific limit)",
+                     "DUT responds with ProvideOfferResponse for each until capacity is reached"),
+            TestStep(3, "TH sends an additional ProvideOffer command beyond DUT's capacity",
+                     "DUT responds with RESOURCE_EXHAUSTED status code"),
+        ]
+        return steps
+
+    def pics_TC_WEBRTCP_2_16(self) -> list[str]:
+        pics = [
+            "WEBRTCP.S",
+            "AVSM.S",
+        ]
+        return pics
+
+    @async_test_body
+    async def test_TC_WEBRTCP_2_16(self):
+        """
+        Executes the test steps for validating ProvideOffer resource exhaustion.
+        """
+
+        endpoint = self.get_endpoint(default=1)
+
+        self.step(1)
+        # Allocate Audio and Video streams
+        audio_stream_id = await self.allocate_one_audio_stream()
+        video_stream_id = await self.allocate_one_video_stream()
+
+        # Validate that the streams were allocated successfully
+        await self.validate_allocated_audio_stream(audio_stream_id)
+        await self.validate_allocated_video_stream(video_stream_id)
+
+        self.step(2)
+        # Send multiple ProvideOffer commands to exhaust the DUT's capacity
+        logger.info("Starting to send ProvideOffer commands to exhaust DUT capacity")
+
+        # Get the maximum concurrent WebRTC sessions from user or use default for CI
+        prompt_msg = (
+            "\nPlease enter the maximum number of concurrent WebRTC sessions supported by the DUT:\n"
+            "This value is DUT-specific and should be obtained from the DUT documentation or specifications.\n"
+            "Enter the number (e.g., 5): "
+        )
+
+        if self.is_pics_sdk_ci_only:
+            # Use default value for CI testing
+            max_attempts = 5
+            logger.info(f"Using default max_attempts={max_attempts} for CI testing")
+        else:
+            # Prompt user for DUT-specific value
+            user_input = self.wait_for_user_input(prompt_msg)
+            try:
+                max_attempts = int(user_input.strip())
+                asserts.assert_true(max_attempts > 0, "Maximum concurrent sessions must be greater than 0")
+                logger.info(f"Using user-specified max_attempts={max_attempts}")
+            except ValueError:
+                asserts.fail(f"Invalid input '{user_input}'. Please enter a valid number.")
+
+        provide_offer_cmd = Clusters.WebRTCTransportProvider.Commands.ProvideOffer(
+            webRTCSessionID=NullValue,
+            sdp=(
+                "v=0\n"
+                "o=rtc 2281582238 0 IN IP4 127.0.0.1\n"
+                "s=-\n"
+                "t=0 0\n"
+                "a=group:BUNDLE 0\n"
+                "a=msid-semantic:WMS *\n"
+                "a=ice-options:ice2,trickle\n"
+                "a=fingerprint:sha-256 8F:BF:9A:B9:FA:59:EC:F6:08:EA:47:D3:F4:AC:FA:AC:E9:27:FA:28:D3:00:1D:9B:EF:62:3F:B8:C6:09:FB:B9\n"
+                "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n"
+                "c=IN IP4 0.0.0.0\n"
+                "a=mid:0\n"
+                "a=sendrecv\n"
+                "a=sctp-port:5000\n"
+                "a=max-message-size:262144\n"
+                "a=setup:actpass\n"
+                "a=ice-ufrag:ytRw\n"
+                "a=ice-pwd:blrzPJtaV9Y1BNgbC1bXpi"
+            ),
+            streamUsage=3,
+            originatingEndpointID=1,
+            videoStreamID=video_stream_id,
+            audioStreamID=audio_stream_id
+        )
+
+        # Try to allocate multiple sessions to reach the DUT's capacity limit
+        for attempt in range(max_attempts):
+            logger.info(f"Attempt {attempt + 1}: Sending ProvideOffer command")
+            resp = await self.send_single_cmd(
+                cmd=provide_offer_cmd,
+                endpoint=1,
+                payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD
+            )
+            asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.ProvideOfferResponse,
+                                 "Incorrect response type")
+            logger.info(f"Successfully created ProvideOffer session {attempt + 1}")
+
+        self.step(3)
+        # Send an additional ProvideOffer command when DUT capacity is exhausted
+        logger.info("Sending additional ProvideOffer command to trigger resource exhaustion")
+
+        # This should fail with RESOURCE_EXHAUSTED status
+        try:
+            resp = await self.send_single_cmd(
+                cmd=provide_offer_cmd,
+                endpoint=endpoint,
+                payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD
+            )
+            # If we reach here, the command succeeded when it should have failed
+            asserts.fail("Expected RESOURCE_EXHAUSTED error but ProvideOffer command succeeded")
+        except InteractionModelError as e:
+            # Verify that we got the expected RESOURCE_EXHAUSTED status
+            asserts.assert_equal(e.status, Status.ResourceExhausted,
+                                 f"Expected RESOURCE_EXHAUSTED status, got {e.status}")
+            logger.info(f"Correctly received RESOURCE_EXHAUSTED status: {e.status}")
+
+        logger.info("Successfully validated ProvideOffer resource exhaustion behavior")
+
+
+if __name__ == "__main__":
+    default_matter_test_main()