@@ -682,15 +682,16 @@ async def test_answer__no_address_logs_error(self, caplog):
682682
683683 @pytest .mark .asyncio
684684 async def test_answer__includes_contact_header (self , fake_rtp_transport ):
685- """Include a Contact header with the local SIP address in 200 OK."""
685+ """Include a Contact header with the local SIPS address in 200 OK."""
686686 protocol = FakeProtocol ()
687687 addr = ("192.0.2.1" , 5060 )
688688 invite = self ._make_invite ("answer-contact-1" )
689689 protocol .request_received (invite , addr )
690690 await self ._run_answer (protocol , invite , fake_rtp_transport )
691691 response , _ = protocol ._sent_responses [- 1 ]
692692 assert "Contact" in response .headers
693- assert response .headers ["Contact" ].startswith ("<sip:" )
693+ # FakeProtocol uses FakeTransport (TLS, not downgraded) → sips:
694+ assert response .headers ["Contact" ].startswith ("<sips:" )
694695
695696 @pytest .mark .asyncio
696697 async def test_answer__includes_allow_header (self , fake_rtp_transport ):
@@ -1631,16 +1632,31 @@ class TestRegistration:
16311632 def test_registrar_uri__strips_user_from_aor (self ):
16321633 """Derive registrar URI from AOR by stripping the user part."""
16331634 p = make_register_session (
aor = "sip:[email protected] " )
1635+ # _is_tls=False by default (no connection_made called)
16341636 assert p .registrar_uri == "sip:example.com"
16351637
16361638 def test_registrar_uri__preserves_port (self ):
16371639 """Preserve a non-default port in the derived registrar URI."""
16381640 p = make_register_session (
aor = "sip:[email protected] :5080" )
16391641 assert p .registrar_uri == "sip:example.com:5080"
16401642
1641- def test_registrar_uri__normalises_sips_to_sip (self ):
1642- """sips: AOR is normalised to sip: in the registrar URI (RFC 5630 §4)."""
1643+ def test_registrar_uri__uses_sips_when_tls (self ):
1644+ """Prefer sips: scheme over TLS before any downgrade (RFC 5630 §4)."""
1645+ p = make_register_session (
aor = "sip:[email protected] " )
1646+ p ._is_tls = True
1647+ assert p .registrar_uri == "sips:example.com"
1648+
1649+ def test_registrar_uri__uses_sip_when_downgraded (self ):
1650+ """Fall back to sip: after a sips: rejection (RFC 5630 §4 downgrade)."""
1651+ p = make_register_session (
aor = "sip:[email protected] " )
1652+ p ._is_tls = True
1653+ p ._sips_downgraded = True
1654+ assert p .registrar_uri == "sip:example.com"
1655+
1656+ def test_registrar_uri__normalises_sips_aor_when_not_tls (self ):
1657+ """sips: AOR normalised to sip: in the registrar URI when not on TLS."""
16431658 p = make_register_session (
aor = "sips:[email protected] " )
1659+ # _is_tls=False → always sip:
16441660 assert p .registrar_uri == "sip:example.com"
16451661
16461662 async def test_connection_made__sends_register (self ):
@@ -1662,7 +1678,8 @@ async def _initialize(self):
16621678 await asyncio .sleep (0.05 )
16631679 transport .write .assert_called ()
16641680 (data ,) = transport .write .call_args [0 ]
1665- assert b"REGISTER sip:example.com SIP/2.0" in data
1681+ # make_mock_transport() simulates TLS → sips: is preferred first attempt.
1682+ assert b"REGISTER sips:example.com SIP/2.0" in data
16661683
16671684 async def test_register__includes_required_headers (self ):
16681685 """REGISTER request includes From, To, Call-ID, CSeq, Contact and Expires."""
@@ -1675,7 +1692,8 @@ async def test_register__includes_required_headers(self):
16751692 (data ,) = transport .write .call_args [0 ]
16761693 assert b"From: sip:[email protected] " in data 16771694 assert b"To: sip:[email protected] " in data 1678- assert b"Contact: <sip:[email protected] :5061;transport=tls>" in data 1695+ # TLS and not downgraded → sips: Contact
1696+ assert b"Contact: <sips:[email protected] :5061>" in data 16791697 assert b"Expires: 3600" in data
16801698
16811699 async def test_register__increments_cseq (self ):
@@ -1855,16 +1873,68 @@ async def test_register__via_branch_is_unique_per_request(self):
18551873 assert branch1 != branch2
18561874
18571875 async def test_register__contact_uses_local_addr (self ):
1858- """Contact header uses sip: URI with transport=tls and the local socket address."""
1876+ """Contact header uses sips: URI when TLS is active (RFC 5630 §4, first attempt)."""
1877+ p = make_register_session ()
1878+ p .local_address = "10.0.0.5" , 5061
1879+ transport = make_mock_transport ("10.0.0.5" , 5061 )
1880+ p .transport = transport
1881+ p ._is_tls = True
1882+ await p .register ()
1883+ (data ,) = transport .write .call_args [0 ]
1884+ assert b"Contact: <sips:[email protected] :5061>" in data 1885+
1886+ async def test_register__contact_uses_sip_transport_tls_when_downgraded (self ):
1887+ """Contact header falls back to sip:;transport=tls after a 403 downgrade."""
18591888 p = make_register_session ()
18601889 p .local_address = "10.0.0.5" , 5061
18611890 transport = make_mock_transport ("10.0.0.5" , 5061 )
18621891 p .transport = transport
18631892 p ._is_tls = True
1893+ p ._sips_downgraded = True
18641894 await p .register ()
18651895 (data ,) = transport .write .call_args [0 ]
18661896 assert b"Contact: <sip:[email protected] :5061;transport=tls>" in data 18671897
1898+ async def test_response_received__403_for_sips_retries_with_sip (self ):
1899+ """403 Forbidden on REGISTER when using sips: triggers a sip:;transport=tls retry."""
1900+ p = make_register_session (username = "alice" , password = "secret" ) # noqa: S106
1901+ transport = make_mock_transport ()
1902+ p .transport = transport
1903+ p .local_address = ("127.0.0.1" , 5061 )
1904+ p ._is_tls = True
1905+ p .response_received (
1906+ Response (
1907+ status_code = 403 ,
1908+ reason = "Forbidden (sips URI unsupported)" ,
1909+ headers = {"CSeq" : "1 REGISTER" },
1910+ ),
1911+ ("192.0.2.2" , 5061 ),
1912+ )
1913+ await asyncio .sleep (0.05 )
1914+ (data ,) = transport .write .call_args [0 ]
1915+ # After downgrade, REGISTER request-URI must use sip: scheme.
1916+ assert b"REGISTER sip:" in data
1917+ # Contact must use sip: with transport=tls, not sips:.
1918+ assert b"Contact: <sip:[email protected] :5061;transport=tls>" in data 1919+
1920+ async def test_response_received__403_not_retriggered_after_downgrade (self ):
1921+ """A second 403 after downgrading does not loop — raises NotImplementedError."""
1922+ p = make_register_session ()
1923+ transport = make_mock_transport ()
1924+ p .transport = transport
1925+ p .local_address = ("127.0.0.1" , 5061 )
1926+ p ._is_tls = True
1927+ p ._sips_downgraded = True # already downgraded
1928+ with pytest .raises (NotImplementedError ):
1929+ p .response_received (
1930+ Response (
1931+ status_code = 403 ,
1932+ reason = "Forbidden" ,
1933+ headers = {"CSeq" : "1 REGISTER" },
1934+ ),
1935+ ("192.0.2.2" , 5061 ),
1936+ )
1937+
18681938 async def test_data_received__sip_response__calls_response_received (self ):
18691939 """data_received routes SIP messages to response_received via TCP stream."""
18701940 received = []
0 commit comments