@@ -345,6 +345,111 @@ func TestParseDMRFromXMLResolvesRelativeServiceURLs(t *testing.T) {
345345 }
346346}
347347
348+ func TestParseDMRFromXMLURLBaseHandling (t * testing.T ) {
349+ tests := []struct {
350+ name string
351+ raw string
352+ wantControl string
353+ wantEventSub string
354+ }{
355+ {
356+ name : "Absolute URLBase Overrides Location Base" ,
357+ raw : `<?xml version="1.0"?>
358+ <root>
359+ <URLBase>http://example.com:8080/upnp/</URLBase>
360+ <device>
361+ <serviceList>
362+ <service>
363+ <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
364+ <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
365+ <controlURL>ctrl</controlURL>
366+ <eventSubURL>event</eventSubURL>
367+ </service>
368+ </serviceList>
369+ </device>
370+ </root>` ,
371+ wantControl : "http://example.com:8080/upnp/ctrl" ,
372+ wantEventSub : "http://example.com:8080/upnp/event" ,
373+ },
374+ {
375+ name : "Relative URLBase Resolved Against Location Base" ,
376+ raw : `<?xml version="1.0"?>
377+ <root>
378+ <URLBase>/upnp/</URLBase>
379+ <device>
380+ <serviceList>
381+ <service>
382+ <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
383+ <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
384+ <controlURL>ctrl</controlURL>
385+ <eventSubURL>event</eventSubURL>
386+ </service>
387+ </serviceList>
388+ </device>
389+ </root>` ,
390+ wantControl : "http://example.com:8080/upnp/ctrl" ,
391+ wantEventSub : "http://example.com:8080/upnp/event" ,
392+ },
393+ {
394+ name : "Malformed URLBase Falls Back To Location Base" ,
395+ raw : `<?xml version="1.0"?>
396+ <root>
397+ <URLBase>http:/upnp</URLBase>
398+ <device>
399+ <serviceList>
400+ <service>
401+ <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
402+ <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
403+ <controlURL>/ctrl</controlURL>
404+ <eventSubURL>/event</eventSubURL>
405+ </service>
406+ </serviceList>
407+ </device>
408+ </root>` ,
409+ wantControl : "http://example.com:8080/ctrl" ,
410+ wantEventSub : "http://example.com:8080/event" ,
411+ },
412+ {
413+ name : "Malformed Service URL Uses URLBase After Sanitization" ,
414+ raw : `<?xml version="1.0"?>
415+ <root>
416+ <URLBase>http://example.com:8080/upnp/</URLBase>
417+ <device>
418+ <serviceList>
419+ <service>
420+ <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
421+ <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
422+ <controlURL>_urn:schemas-upnp-org:service:AVTransport_control</controlURL>
423+ <eventSubURL>_urn:schemas-upnp-org:service:AVTransport_event</eventSubURL>
424+ </service>
425+ </serviceList>
426+ </device>
427+ </root>` ,
428+ wantControl : "http://example.com:8080/upnp/_urn:schemas-upnp-org:service:AVTransport_control" ,
429+ wantEventSub : "http://example.com:8080/upnp/_urn:schemas-upnp-org:service:AVTransport_event" ,
430+ },
431+ }
432+
433+ locationBase , _ := url .Parse ("http://example.com:8080/xml/device_description.xml" )
434+
435+ for _ , tt := range tests {
436+ t .Run (tt .name , func (t * testing.T ) {
437+ result , err := ParseDMRFromXML ([]byte (tt .raw ), locationBase )
438+ if err != nil {
439+ t .Fatalf ("ParseDMRFromXML() unexpected error: %v" , err )
440+ }
441+
442+ if result .AvtransportControlURL != tt .wantControl {
443+ t .Fatalf ("AvtransportControlURL = %q, want %q" , result .AvtransportControlURL , tt .wantControl )
444+ }
445+
446+ if result .AvtransportEventSubURL != tt .wantEventSub {
447+ t .Fatalf ("AvtransportEventSubURL = %q, want %q" , result .AvtransportEventSubURL , tt .wantEventSub )
448+ }
449+ })
450+ }
451+ }
452+
348453func TestDMRextractorEmbeddedDevice (t * testing.T ) {
349454 // Test full HTTP flow with embedded device XML
350455 raw := `<?xml version="1.0" encoding="utf-8"?>
@@ -520,6 +625,64 @@ func TestParseAllDMRFromXML(t *testing.T) {
520625 }
521626}
522627
628+ func TestParseAllDMRFromXMLUsesURLBase (t * testing.T ) {
629+ raw := `<?xml version="1.0"?>
630+ <root>
631+ <URLBase>http://example.com:8080/upnp/</URLBase>
632+ <device>
633+ <deviceList>
634+ <device>
635+ <friendlyName>Zone 1</friendlyName>
636+ <serviceList>
637+ <service>
638+ <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
639+ <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
640+ <controlURL>zone1/AVTransport</controlURL>
641+ <eventSubURL>zone1/event</eventSubURL>
642+ </service>
643+ </serviceList>
644+ </device>
645+ <device>
646+ <friendlyName>Zone 2</friendlyName>
647+ <serviceList>
648+ <service>
649+ <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
650+ <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
651+ <controlURL>zone2/AVTransport</controlURL>
652+ <eventSubURL>zone2/event</eventSubURL>
653+ </service>
654+ </serviceList>
655+ </device>
656+ </deviceList>
657+ </device>
658+ </root>`
659+
660+ baseURL , _ := url .Parse ("http://example.com:8080/xml/device_description.xml" )
661+ results , err := ParseAllDMRFromXML ([]byte (raw ), baseURL )
662+ if err != nil {
663+ t .Fatalf ("ParseAllDMRFromXML() unexpected error: %v" , err )
664+ }
665+
666+ if len (results ) != 2 {
667+ t .Fatalf ("ParseAllDMRFromXML() returned %d devices, want 2" , len (results ))
668+ }
669+
670+ expectedControls := map [string ]string {
671+ "Zone 1" : "http://example.com:8080/upnp/zone1/AVTransport" ,
672+ "Zone 2" : "http://example.com:8080/upnp/zone2/AVTransport" ,
673+ }
674+
675+ for _ , dev := range results {
676+ want , ok := expectedControls [dev .FriendlyName ]
677+ if ! ok {
678+ t .Fatalf ("unexpected device in results: %q" , dev .FriendlyName )
679+ }
680+ if dev .AvtransportControlURL != want {
681+ t .Fatalf ("AvtransportControlURL for %q = %q, want %q" , dev .FriendlyName , dev .AvtransportControlURL , want )
682+ }
683+ }
684+ }
685+
523686func TestLoadDevicesFromLocationMultiple (t * testing.T ) {
524687 // Test full HTTP flow with multiple MediaRenderers
525688 raw := `<?xml version="1.0"?>
0 commit comments