@@ -414,13 +414,60 @@ def test_auto_create_cert_with_long_device_name(self):
414414 client .full_clean ()
415415 client .save ()
416416 # The last 9 characters gets truncated and replaced with unique id
417- self .assertIn (
418- "{mac_address}-{name}" .format (** d .__dict__ )[:- 9 ], client ._get_common_name ()
419- )
417+ mac = d .mac_address
418+ self .assertIn (f"{ mac } -{ device_name } " [:- 9 ], client ._get_common_name ())
420419 self .assertEqual (len (client ._get_common_name ()), 64 )
421420 cert = Cert .objects .filter (organization = org , name = device_name )
422421 self .assertEqual (cert .count (), 1 )
423422 self .assertEqual (cert .first ().common_name [:- 9 ], client ._get_common_name ()[:- 9 ])
423+ # The device name must NOT be mutated by _get_common_name()
424+ self .assertEqual (d .name , device_name )
425+
426+ def test_get_common_name_does_not_mutate_device_name (self ):
427+ """
428+ _get_common_name() must not modify the device.name attribute.
429+ Django caches FK targets, so mutating the device inside _get_common_name()
430+ would corrupt the caller's reference to the same object and could cause
431+ a truncated name to be saved to the database on a subsequent device.save().
432+ """
433+ # Use a name longer than the truncation threshold (63 - len(mac) = 46 chars
434+ # for a standard 17-char MAC address).
435+ long_name = "a" * 50
436+ org = self ._create_org (name = "org1" )
437+ vpn = self ._create_vpn (organization = org )
438+ d = self ._create_device (organization = org , name = long_name )
439+ c = self ._create_config (device = d )
440+ client = VpnClient (vpn = vpn , config = c , auto_cert = True )
441+
442+ client ._get_common_name ()
443+
444+ # The in-memory device name must remain unchanged after _get_common_name().
445+ self .assertEqual (d .name , long_name )
446+ # The database record must also be unaffected.
447+ self .assertEqual (
448+ d .__class__ .objects .values_list ("name" , flat = True ).get (pk = d .pk ),
449+ long_name ,
450+ )
451+
452+ def test_get_common_name_does_not_mutate_device_name_short_name (self ):
453+ """
454+ For device names below the truncation threshold _get_common_name()
455+ must still not touch the device object even though the slice is a no-op.
456+ """
457+ short_name = "router-01"
458+ org = self ._create_org (name = "org1" )
459+ vpn = self ._create_vpn (organization = org )
460+ d = self ._create_device (organization = org , name = short_name )
461+ c = self ._create_config (device = d )
462+ client = VpnClient (vpn = vpn , config = c , auto_cert = True )
463+
464+ # Capture the actual string object stored on the device before the call.
465+ name_before = d .__dict__ ["name" ]
466+ client ._get_common_name ()
467+
468+ self .assertEqual (d .name , short_name )
469+ # The same string object must still be on the device — not a fresh slice.
470+ self .assertIs (d .__dict__ ["name" ], name_before )
424471
425472 @mock .patch .object (Vpn , "dhparam" , side_effect = SoftTimeLimitExceeded )
426473 def test_update_vpn_dh_timeout (self , dhparam ):
0 commit comments