Implements test case TC_WEBRTCP_2_4 (#38527)

* Implements test case TC_WEBRTCP_2_4

* Restyled by autopep8

---------

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/python_testing/TC_WEBRTCP_2_4.py b/src/python_testing/TC_WEBRTCP_2_4.py
new file mode 100644
index 0000000..dc1b83d
--- /dev/null
+++ b/src/python_testing/TC_WEBRTCP_2_4.py
@@ -0,0 +1,249 @@
+#
+#  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 chip.clusters as Clusters
+from chip import ChipDeviceCtrl
+from chip.clusters.Types import NullValue
+from chip.interaction_model import InteractionModelError, Status
+from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from mobly import asserts
+
+
+class TC_WebRTCProvider_2_4(MatterBaseTest):
+
+    def desc_TC_WebRTCProvider_2_4(self) -> str:
+        """Returns a description of this test"""
+        return "[TC-{picsCode}-2.4] Validate setting an SDP Offer for an existing session with {DUT_Server}"
+
+    def steps_TC_WebRTCProvider_2_4(self) -> list[TestStep]:
+        """
+        Define the step-by-step sequence for the test.
+        """
+        steps = [
+            TestStep(1, "Read CurrentSessions attribute => expect 0", is_commissioning=True),
+            TestStep(2, "Send ProvideOffer with non‑existent WebRTCSessionID => expect NotFound error"),
+            TestStep(3, "Send ProvideOffer with null session/video/audio IDs => expect ProvideOfferResponse (allocated IDs)"),
+            TestStep(4, "Read CurrentSessions => expect 1 (save IDs)"),
+            TestStep(5, "Send ProvideOffer with (saved WebRTCSessionID + 1) => expect NotFound error"),
+            TestStep(6, "Send ProvideOffer with saved WebRTCSessionID (re‑offer) => expect ProvideOfferResponse with same IDs"),
+        ]
+        return steps
+
+    @async_test_body
+    async def test_TC_WebRTCProvider_2_4(self):
+        """
+        Executes the test steps for the WebRTC Provider cluster scenario.
+        """
+
+        endpoint = self.get_endpoint(default=1)
+        cluster = Clusters.WebRTCTransportProvider
+
+        self.step(1)
+        current_sessions = await self.read_single_attribute_check_success(
+            endpoint=endpoint,
+            cluster=cluster,
+            attribute=cluster.Attributes.CurrentSessions
+        )
+        asserts.assert_equal(len(current_sessions), 0, "CurrentSessions must be empty!")
+
+        self.step(2)
+        nonexistent_session_id = 1
+        cmd = cluster.Commands.ProvideOffer(
+            webRTCSessionID=nonexistent_session_id,
+            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=endpoint,
+            videoStreamID=NullValue,
+            audioStreamID=NullValue,
+        )
+        try:
+            await self.send_single_cmd(cmd=cmd, endpoint=endpoint, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
+            asserts.fail("Unexpected success on ProvideOffer")
+        except InteractionModelError as e:
+            asserts.assert_equal(e.status, Status.NotFound, "ProvideOffer should return NotFound for unknown session")
+
+        self.step(3)
+        cmd = cluster.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=endpoint,
+            videoStreamID=NullValue,
+            audioStreamID=NullValue
+        )
+        resp = await self.send_single_cmd(cmd=cmd, endpoint=endpoint, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
+        asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.ProvideOfferResponse,
+                             "Incorrect response type")
+        saved_session_id = resp.webRTCSessionID
+        saved_video_id = resp.videoStreamID
+        saved_audio_id = resp.audioStreamID
+        asserts.assert_not_equal(
+            saved_session_id, 0, "Allocated WebRTCSessionID must be non‑zero"
+        )
+
+        self.step(4)
+        current_sessions = await self.read_single_attribute_check_success(
+            endpoint=endpoint,
+            cluster=cluster,
+            attribute=cluster.Attributes.CurrentSessions,
+        )
+        asserts.assert_equal(
+            len(current_sessions),
+            1,
+            f"Expected exactly one CurrentSession, got {len(current_sessions)}",
+        )
+
+        self.step(5)
+        wrong_session_id = saved_session_id + 1
+        cmd = cluster.Commands.ProvideOffer(
+            webRTCSessionID=wrong_session_id,
+            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=endpoint,
+            videoStreamID=saved_video_id,
+            audioStreamID=saved_audio_id,
+        )
+        try:
+            await self.send_single_cmd(cmd=cmd, endpoint=endpoint, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
+            asserts.fail("ProvideOffer unexpectedly succeeded for wrong session ID")
+        except InteractionModelError as e:
+            asserts.assert_equal(e.status, Status.NotFound, "ProvideOffer should return NotFound for wrong session ID")
+
+        self.step(6)
+        cmd = cluster.Commands.ProvideOffer(
+            webRTCSessionID=saved_session_id,
+            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=endpoint,
+            videoStreamID=saved_video_id,
+            audioStreamID=saved_audio_id,
+        )
+        resp = await self.send_single_cmd(cmd=cmd, endpoint=endpoint, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
+        asserts.assert_equal(type(resp), Clusters.WebRTCTransportProvider.Commands.ProvideOfferResponse,
+                             "Incorrect response type")
+        asserts.assert_equal(
+            resp.webRTCSessionID,
+            saved_session_id,
+            "SessionID in response must match the existing session",
+        )
+        asserts.assert_equal(
+            resp.videoStreamID,
+            saved_video_id,
+            "VideoStreamID in response changed unexpectedly",
+        )
+        asserts.assert_equal(
+            resp.audioStreamID,
+            saved_audio_id,
+            "AudioStreamID in response changed unexpectedly",
+        )
+
+
+if __name__ == "__main__":
+    default_matter_test_main()