upnp messages tests

This commit is contained in:
xis 2023-10-16 18:02:47 +02:00
parent e7332d94b3
commit e2c23b80e6
2 changed files with 127 additions and 20 deletions

View file

@ -1,32 +1,43 @@
package net.schowek.nextclouddlna.controller package net.schowek.nextclouddlna.controller
import net.schowek.nextclouddlna.DlnaService
import net.schowek.nextclouddlna.dlna.media.MediaServer import net.schowek.nextclouddlna.dlna.media.MediaServer
import org.jupnp.support.contentdirectory.DIDLParser
import org.jupnp.support.model.DIDLObject
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.InputSource import org.xml.sax.InputSource
import support.IntegrationSpecification import support.IntegrationSpecification
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPath import javax.xml.xpath.XPath
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory import javax.xml.xpath.XPathFactory
import static javax.xml.xpath.XPathConstants.NODE import static javax.xml.xpath.XPathConstants.NODE
import static javax.xml.xpath.XPathConstants.NODESET
import static org.jupnp.support.model.WriteStatus.NOT_WRITABLE
class UpnpControllerIntTest extends IntegrationSpecification { class UpnpControllerIntTest extends IntegrationSpecification {
@Autowired @Autowired
private MediaServer mediaServer private MediaServer mediaServer
@Autowired
private DlnaService dlnaService
def uid
def setup() {
uid = mediaServer.serviceIdentifier
}
def "should serve icon"() { def "should serve icon"() {
given:
def uid = mediaServer.serviceIdentifier
when: when:
ResponseEntity<byte[]> response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/icon.png"), byte[]); def response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/icon.png"), byte[]);
then: then:
response.statusCode == HttpStatus.OK response.statusCode == HttpStatus.OK
@ -36,11 +47,8 @@ class UpnpControllerIntTest extends IntegrationSpecification {
} }
def "should serve service descriptor"() { def "should serve service descriptor"() {
given:
def uid = mediaServer.serviceIdentifier
when: when:
ResponseEntity<String> response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/desc"), String); def response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/desc"), String);
then: then:
response.statusCode == HttpStatus.OK response.statusCode == HttpStatus.OK
@ -48,6 +56,7 @@ class UpnpControllerIntTest extends IntegrationSpecification {
assert it['content-type'] == ['text/xml'] assert it['content-type'] == ['text/xml']
} }
when:
Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
new InputSource(new StringReader(response.body)) new InputSource(new StringReader(response.body))
); );
@ -58,8 +67,113 @@ class UpnpControllerIntTest extends IntegrationSpecification {
nodeValue(dom, "/root/device/presentationURL") == urlWithPort() nodeValue(dom, "/root/device/presentationURL") == urlWithPort()
} }
def "should serve content directory desc"() {
when:
def response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/svc/upnp-org/ContentDirectory/desc"), String);
then:
response.statusCode == HttpStatus.OK
with(response.headers.each { it.key.toLowerCase() }) {
assert it['content-type'] == ['text/xml']
}
when:
Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
new InputSource(new StringReader(response.body))
);
then:
with(node(dom, "/scpd/serviceStateTable/stateVariable[name='A_ARG_TYPE_BrowseFlag']/allowedValueList").childNodes) {
assert it.length == 2
assert it.item(0).textContent == "BrowseMetadata"
assert it.item(1).textContent == "BrowseDirectChildren"
}
}
def "should serve connectionMgr desc"() {
when:
def response = restTemplate().getForEntity(urlWithPort("/dev/${uid}/svc/upnp-org/ConnectionManager/desc"), String);
then:
response.statusCode == HttpStatus.OK
with(response.headers.each { it.key.toLowerCase() }) {
assert it['content-type'] == ['text/xml']
}
when:
Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
new InputSource(new StringReader(response.body))
);
then:
with(nodeList(dom, "/scpd/actionList/action/name")) {
assert it.length == 3
it.item(0).textContent == "GetCurrentConnectionIDs"
it.item(1).textContent == "GetProtocolInfo"
it.item(2).textContent == "GetCurrentConnectionInfo"
}
}
def "should handle upnp browse ROOT request"() {
given:
def requestBody = '<?xml version="1.0" encoding="utf-8"?>\n' +
'<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"\n' +
' s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\n' +
' <s:Body>\n' +
' <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">\n' +
' <ObjectID>0</ObjectID>\n' +
' <BrowseFlag>BrowseMetadata</BrowseFlag>\n' +
' <Filter>*</Filter>\n' +
' <StartingIndex>0</StartingIndex>\n' +
' <RequestedCount>0</RequestedCount>\n' +
' <SortCriteria></SortCriteria>\n' +
' </u:Browse>\n' +
' </s:Body>\n' +
'</s:Envelope>'
when:
def headers = new HttpHeaders([
'content-type': 'text/xml; charset="utf-8"',
'soapaction' : '"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"'
]);
HttpEntity<String> request = new HttpEntity<String>(requestBody, headers);
def response = restTemplate().postForEntity(urlWithPort("/dev/$uid/svc/upnp-org/ContentDirectory/action"), request, String)
then:
response.statusCode == HttpStatus.OK
response.headers['content-type'].find() == "text/xml;charset=utf-8"
when:
Document dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
new InputSource(new StringReader(response.body))
);
def didl = new DIDLParser().parse(nodeValue(dom, "/Envelope/Body/BrowseResponse/Result"))
then:
didl.containers.size() == 1
with(didl.containers[0]) {
assert id == "0"
assert parentID == "-1"
assert searchable
assert restricted
assert title == "ROOT"
assert writeStatus == NOT_WRITABLE
assert clazz.value == new DIDLObject.Class("object.container").value
assert childCount == 3 // johndoe, janedoe, family folder
}
}
private String nodeValue(Document dom, String pattern) { private String nodeValue(Document dom, String pattern) {
return node(dom, "$pattern/text()").nodeValue
}
private Node node(Document dom, String pattern) {
XPath xpath = XPathFactory.newInstance().newXPath(); XPath xpath = XPathFactory.newInstance().newXPath();
return (xpath.evaluate("$pattern/text()", dom, NODE) as Node).nodeValue return (xpath.evaluate(pattern, dom, NODE) as Node)
}
private NodeList nodeList(Document dom, String pattern) {
XPath xpath = XPathFactory.newInstance().newXPath();
return (xpath.evaluate(pattern, dom, NODESET) as NodeList)
} }
} }

View file

@ -16,23 +16,16 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
@SpringBootTest(webEnvironment = DEFINED_PORT) @SpringBootTest(webEnvironment = DEFINED_PORT)
@ActiveProfiles("integration") @ActiveProfiles("integration")
class IntegrationSpecification extends Specification { class IntegrationSpecification extends Specification {
private TestRestTemplate restTemplate = new TestRestTemplate() @Autowired
private TestRestTemplate restTemplate
// TODO BEAN
TestRestTemplate restTemplate() { TestRestTemplate restTemplate() {
if (restTemplate == null) {
restTemplate = new TestRestTemplate()
}
return restTemplate return restTemplate
} }
@Autowired @Autowired
private ServerInfoProvider serverInfoProvider private ServerInfoProvider serverInfoProvider
def setup() {
System.err.println("SETUP PARENT")
}
protected String urlWithPort(String uri = "") { protected String urlWithPort(String uri = "") {
return "http://localhost:" + serverInfoProvider.port + uri; return "http://localhost:" + serverInfoProvider.port + uri;
} }