Fix retrieving operations resource 40/106140/1
authorYaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Tue, 10 Jan 2023 09:39:21 +0000 (11:39 +0200)
committerYaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Wed, 24 May 2023 06:06:08 +0000 (09:06 +0300)
Fix retrieving operations resource from controller and mount points
by adapting getOperationsJSON/XML to accept identifiers and enhancing
its functionality with ability to match exact operation.

This way we enhance solution proposed by NETCONF-822 to cover
all use cases and avoid error 500.

JIRA: NETCONF-928
JIRA: NETCONF-929
Change-Id: Ic8f0ca5b07e060d36747a110f9049d3d9a2b96c8
Signed-off-by: Yaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
Signed-off-by: Peter Suna <peter.suna@pantheon.tech>
netconf/netconf-test-models/src/main/yang/module1.yang [new file with mode: 0644]
netconf/netconf-test-models/src/main/yang/module2.yang [new file with mode: 0644]
restconf/restconf-nb/pom.xml
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/api/RestconfOperationsService.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/OperationsContent.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfOperationsServiceImpl.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/Netconf822Test.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfOperationsServiceTest.java
restconf/restconf-nb/src/test/resources/nc822/foo@2021-09-30.yang

diff --git a/netconf/netconf-test-models/src/main/yang/module1.yang b/netconf/netconf-test-models/src/main/yang/module1.yang
new file mode 100644 (file)
index 0000000..b1ce189
--- /dev/null
@@ -0,0 +1,11 @@
+module module1 {
+    namespace "module:1";
+    prefix "mod1";
+    revision "2014-01-01";
+
+    rpc dummy-rpc1-module1 {
+    }
+
+    rpc dummy-rpc2-module1 {
+    }
+}
\ No newline at end of file
diff --git a/netconf/netconf-test-models/src/main/yang/module2.yang b/netconf/netconf-test-models/src/main/yang/module2.yang
new file mode 100644 (file)
index 0000000..fa792d7
--- /dev/null
@@ -0,0 +1,11 @@
+module module2 {
+    namespace "module:2";
+    prefix "mod2";
+    revision "2014-01-02";
+
+    rpc dummy-rpc1-module2 {
+    }
+
+    rpc dummy-rpc2-module2 {
+    }
+}
\ No newline at end of file
index 2379c78f0ce05a10591a6cfed43a9edcdada903c..bcd222da0c35630e54553ce5adec0d32e1ed598e 100644 (file)
       <version>2.36.1</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.opendaylight.netconf</groupId>
+      <artifactId>netconf-test-models</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
index 80c07d6aeb242d3f3207c32b11aabc7bdd44957c..f6ddd5bb31051efa712137c69fb61b823458609f 100644 (file)
@@ -11,11 +11,8 @@ import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.UriInfo;
 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 
 /**
  * Container that provides access to the data-model specific operations supported by the server.
@@ -31,10 +28,21 @@ public interface RestconfOperationsService {
     @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
     String getOperationsJSON();
 
+    /**
+     * Retrieve list of operations and actions supported by the server or device in JSON format.
+     *
+     * @param identifier path parameter to identify device and/or operation
+     * @return A string containing a JSON document conforming to both RFC8040 and RFC7951.
+     */
+    @GET
+    @Path("/operations/{identifier:.+}")
+    @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
+    String getOperationJSON(@PathParam("identifier") String identifier);
+
     /**
      * List RPC and action operations in RFC8040 XML format.
      *
-     * @return A string containing a JSON document conforming to both RFC8040 section 11.3.1 and page 84.
+     * @return A string containing an XML document conforming to both RFC8040 section 11.3.1 and page 84.
      */
     @GET
     @Path("/operations")
@@ -42,20 +50,13 @@ public interface RestconfOperationsService {
     String getOperationsXML();
 
     /**
-     * Valid for mount points. List of operations supported by the server.
+     * Retrieve list of operations and actions supported by the server or device in XML format.
      *
-     * @param identifier path parameter
-     * @param uriInfo URI information
-     * @return {@link NormalizedNodePayload}
+     * @param identifier path parameter to identify device and/or operation
+     * @return A string containing an XML document conforming to both RFC8040 section 11.3.1 and page 84.
      */
     @GET
     @Path("/operations/{identifier:.+}")
-    @Produces({
-        MediaTypes.APPLICATION_YANG_DATA_JSON,
-        MediaTypes.APPLICATION_YANG_DATA_XML,
-        MediaType.APPLICATION_JSON,
-        MediaType.APPLICATION_XML,
-        MediaType.TEXT_XML
-    })
-    NormalizedNodePayload getOperations(@PathParam("identifier") String identifier, @Context UriInfo uriInfo);
+    @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+    String getOperationXML(@PathParam("identifier") String identifier);
 }
index 725e3766d1116858a05a8d5e140ddeea64f66d32..065c702221b6cabdc915ed6035c9d9237614e0cc 100644 (file)
@@ -14,6 +14,8 @@ import java.util.Map;
 import java.util.Map.Entry;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
@@ -100,35 +102,79 @@ enum OperationsContent {
      * @return Content of HTTP GET operation as a String
      */
     public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
-        if (context == null) {
-            // Defensive, return empty content
+        if (isEmptyContext(context)) {
+            // No modules, or defensive return empty content
             return emptyBody;
         }
-        final var modules = context.getModuleStatements();
-        if (modules.isEmpty()) {
-            // No modules, return empty content
+
+        final var moduleRpcs = getModuleRpcs(context, context.getModuleStatements());
+        return moduleRpcs.isEmpty() ? emptyBody : createBody(moduleRpcs);
+    }
+
+    /**
+     * Return content with RPCs and actions for a particular {@link InstanceIdentifierContext}.
+     *
+     * @param identifierContext InstanceIdentifierContext to use
+     * @return Content of HTTP GET operation as a String
+     */
+    public final @NonNull String bodyFor(final @NonNull InstanceIdentifierContext identifierContext) {
+        final var context = identifierContext.getSchemaContext();
+        if (isEmptyContext(context)) {
+            // No modules, or defensive return empty content
             return emptyBody;
         }
 
-        final var moduleRpcs = modules.values().stream()
-            // Extract XMLNamespaces
-            .map(module -> module.localQNameModule().getNamespace())
-            // Make sure each is XMLNamespace unique
-            .distinct()
-            // Find the most recent module with that namespace. This needed so we expose the right set of RPCs,
-            // as we always pick the latest revision to resolve prefix (or module name).
-            .map(namespace -> context.findModuleStatements(namespace).iterator().next())
-            // Convert to module prefix + List<String> with RPC names
-            .map(module -> Map.entry(prefix(module), module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
-                .map(rpc -> rpc.argument().getLocalName())
-                .toList()))
-            // Skip prefixes which do not have any RPCs
-            .filter(entry -> !entry.getValue().isEmpty())
-            // Ensure stability: sort by prefix
-            .sorted(Entry.comparingByKey())
-            .toList();
+        final var stack = identifierContext.inference().toSchemaInferenceStack();
+        // empty stack == get all RPCs/actions
+        if (stack.isEmpty()) {
+            return createBody(getModuleRpcs(context, context.getModuleStatements()));
+        }
+
+        // get current module RPCs/actions by RPC/action name
+        final var currentModule = stack.currentModule();
+        final var currentModuleKey = Map.of(currentModule.localQNameModule(), currentModule);
+        final var rpcName = identifierContext.getSchemaNode().getQName().getLocalName();
+        return getModuleRpcs(context, currentModuleKey).stream()
+            .findFirst()
+            .map(e -> Map.entry(e.getKey(), e.getValue().stream().filter(rpcName::equals).toList()))
+            .map(e -> createBody(List.of(e)))
+            .orElse(emptyBody);
+    }
 
-        return moduleRpcs.isEmpty() ? emptyBody : createBody(moduleRpcs);
+    private static boolean isEmptyContext(final EffectiveModelContext context) {
+        if (context == null) {
+            return true;
+        }
+        return context.getModuleStatements().isEmpty();
+    }
+
+    /**
+     * Returns a list of entries, where each entry contains a module prefix and a list of RPC names.
+     *
+     * @param context the effective model context
+     * @param modules the map of QNameModule to ModuleEffectiveStatement
+     * @return a list of entries, where each entry contains a module prefix and a list of RPC names
+     */
+    private List<Entry<@NonNull String, List<String>>> getModuleRpcs(final EffectiveModelContext context,
+            final Map<QNameModule, ModuleEffectiveStatement> modules) {
+        return modules.values().stream()
+                // Extract XMLNamespaces
+                .map(module -> module.localQNameModule().getNamespace())
+                // Make sure each is XMLNamespace unique
+                .distinct()
+                // Find the most recent module with that namespace. This needed so we expose the right set of RPCs,
+                // as we always pick the latest revision to resolve prefix (or module name).
+                .map(namespace -> context.findModuleStatements(namespace).iterator().next())
+                // Convert to module prefix + List<String> with RPC names
+                .map(module -> Map.entry(prefix(module),
+                        module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
+                        .map(rpc -> rpc.argument().getLocalName())
+                        .toList()))
+                // Skip prefixes which do not have any RPCs
+                .filter(entry -> !entry.getValue().isEmpty())
+                // Ensure stability: sort by prefix
+                .sorted(Entry.comparingByKey())
+                .toList();
     }
 
     abstract @NonNull String createBody(List<Entry<String, List<String>>> rpcsByPrefix);
index aa6dbd4475b7cc3fc63543bda9421c5017c7ec9a..67f28e1c8dcb0bc96fd52137e8847b9f36054c1e 100644 (file)
@@ -9,39 +9,13 @@ package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.collect.ImmutableSet;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Optional;
 import javax.ws.rs.Path;
-import javax.ws.rs.core.UriInfo;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.mdsal.dom.api.DOMMountPoint;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.mdsal.dom.api.DOMSchemaService;
-import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfOperationsService;
-import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
-import org.opendaylight.yangtools.yang.common.Empty;
-import org.opendaylight.yangtools.yang.common.ErrorTag;
-import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
-import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
-import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.Module;
-import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -73,67 +47,21 @@ public class RestconfOperationsServiceImpl implements RestconfOperationsService
     }
 
     @Override
-    public String getOperationsXML() {
-        return OperationsContent.XML.bodyFor(schemaContextHandler.get());
+    public String getOperationJSON(final String identifier) {
+        final var identifierContext = ParserIdentifier.toInstanceIdentifier(identifier,
+                schemaContextHandler.get(), Optional.of(mountPointService));
+        return OperationsContent.JSON.bodyFor(identifierContext);
     }
 
     @Override
-    public NormalizedNodePayload getOperations(final String identifier, final UriInfo uriInfo) {
-        if (!identifier.contains(RestconfConstants.MOUNT)) {
-            final var errMsg = """
-                    URI has bad format. If operations behind mount point should be showed, URI has to end with %s.
-                    """.formatted(RestconfConstants.MOUNT);
-            LOG.debug("{} for {}", errMsg, identifier);
-            throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
-        }
-
-        final InstanceIdentifierContext mountPointIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
-            schemaContextHandler.get(), Optional.of(mountPointService));
-        final DOMMountPoint mountPoint = mountPointIdentifier.getMountPoint();
-        final var entry = contextForModelContext(modelContext(mountPoint), mountPoint);
-        return NormalizedNodePayload.of(entry.getKey(), entry.getValue());
-    }
-
-    private static EffectiveModelContext modelContext(final DOMMountPoint mountPoint) {
-        return mountPoint.getService(DOMSchemaService.class)
-            .flatMap(svc -> Optional.ofNullable(svc.getGlobalContext()))
-            .orElse(null);
+    public String getOperationsXML() {
+        return OperationsContent.XML.bodyFor(schemaContextHandler.get());
     }
 
-    // FIXME: remove this method and everything it uses
-    @Deprecated(forRemoval = true, since = "4.0.0")
-    private static @NonNull Entry<InstanceIdentifierContext, ContainerNode> contextForModelContext(
-            final @NonNull EffectiveModelContext context, final @Nullable DOMMountPoint mountPoint) {
-        // Determine which modules we need and construct leaf schemas to correspond to all RPC definitions
-        final Collection<Module> modules = new ArrayList<>();
-        final ArrayList<OperationsLeafSchemaNode> rpcLeafSchemas = new ArrayList<>();
-        for (final Module m : context.getModules()) {
-            final Collection<? extends RpcDefinition> rpcs = m.getRpcs();
-            if (!rpcs.isEmpty()) {
-                modules.add(new OperationsImportedModule(m));
-                rpcLeafSchemas.ensureCapacity(rpcLeafSchemas.size() + rpcs.size());
-                for (RpcDefinition rpc : rpcs) {
-                    rpcLeafSchemas.add(new OperationsLeafSchemaNode(rpc));
-                }
-            }
-        }
-
-        // Now generate a module for RESTCONF so that operations contain what they need
-        final OperationsContainerSchemaNode operatationsSchema = new OperationsContainerSchemaNode(rpcLeafSchemas);
-        modules.add(new OperationsRestconfModule(operatationsSchema));
-
-        // Now build the operations container and combine it with the context
-        final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> operationsBuilder = Builders.containerBuilder()
-                .withNodeIdentifier(new NodeIdentifier(OperationsContainerSchemaNode.QNAME));
-        for (final OperationsLeafSchemaNode leaf : rpcLeafSchemas) {
-            operationsBuilder.withChild(ImmutableNodes.leafNode(leaf.getQName(), Empty.value()));
-        }
-
-        final var opContext = new OperationsEffectiveModuleContext(ImmutableSet.copyOf(modules));
-        final var stack = SchemaInferenceStack.of(opContext);
-        stack.enterSchemaTree(operatationsSchema.getQName());
-
-        return Map.entry(InstanceIdentifierContext.ofStack(stack, mountPoint), operationsBuilder.build());
+    @Override
+    public String getOperationXML(final String identifier) {
+        final var identifierContext = ParserIdentifier.toInstanceIdentifier(identifier,
+                schemaContextHandler.get(), Optional.of(mountPointService));
+        return OperationsContent.XML.bodyFor(identifierContext);
     }
-
 }
index 9c66d59350fdada043d93f357b84ec22e8829f6d..8aae4fea69d73a483eda27863f6f5580b4bf5749 100644 (file)
@@ -11,32 +11,63 @@ import static org.junit.Assert.assertEquals;
 
 import org.junit.BeforeClass;
 import org.junit.Test;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
 
 public class Netconf822Test {
-    private static EffectiveModelContext CONTEXT;
+    private static InstanceIdentifierContext INSTANCE_IDENTIFIER_CONTEXT;
 
     @BeforeClass
     public static void beforeClass() {
-        CONTEXT = YangParserTestUtils.parseYangResourceDirectory("/nc822");
+        final var context = YangParserTestUtils.parseYangResourceDirectory("/nc822");
+        final var stack = SchemaInferenceStack.of(context, Absolute.of(QName.create("foo", "new1",
+            Revision.of("2021-09-30"))));
+        INSTANCE_IDENTIFIER_CONTEXT = InstanceIdentifierContext.ofStack(stack);
     }
 
     @Test
     public void testOperationsContentJSON() {
-        assertEquals("{\n"
-            + "  \"ietf-restconf:operations\" : {\n"
-            + "    \"foo:new\": [null]\n"
-            + "  }\n"
-            + "}", OperationsContent.JSON.bodyFor(CONTEXT));
+        assertEquals("""
+            {
+              "ietf-restconf:operations" : {
+                "foo:new": [null],
+                "foo:new1": [null]
+              }
+            }""", OperationsContent.JSON.bodyFor(INSTANCE_IDENTIFIER_CONTEXT.getSchemaContext()));
+    }
+
+    @Test
+    public void testOperationsContentByIdentifierJSON() {
+        assertEquals("""
+            {
+              "ietf-restconf:operations" : {
+                "foo:new1": [null]
+              }
+            }""", OperationsContent.JSON.bodyFor(INSTANCE_IDENTIFIER_CONTEXT));
     }
 
     @Test
     public void testOperationsContentXML() {
-        assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"\n"
-            + "            xmlns:ns0=\"foo\" >\n"
-            + "  <ns0:new/>\n"
-            + "</operations>", OperationsContent.XML.bodyFor(CONTEXT));
+        assertEquals("""
+            <?xml version="1.0" encoding="UTF-8"?>
+            <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
+                        xmlns:ns0="foo" >
+              <ns0:new/>
+              <ns0:new1/>
+            </operations>""", OperationsContent.XML.bodyFor(INSTANCE_IDENTIFIER_CONTEXT.getSchemaContext()));
+    }
+
+    @Test
+    public void testOperationsContentByIdentifierXML() {
+        assertEquals("""
+            <?xml version="1.0" encoding="UTF-8"?>
+            <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
+                        xmlns:ns0="foo" >
+              <ns0:new1/>
+            </operations>""", OperationsContent.XML.bodyFor(INSTANCE_IDENTIFIER_CONTEXT));
     }
 }
index e65a8cdbba9550c9864a078bc6f7bf8efcf1fd49..d5090953ef67b8b5f922e8dbd10e477636ce0c6e 100644 (file)
 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 
-import java.io.IOException;
+import java.util.Optional;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.binding.runtime.spi.BindingRuntimeHelpers;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMMountPoint;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
-import org.opendaylight.restconf.nb.rfc8040.TestUtils;
-import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+import org.opendaylight.mdsal.dom.api.DOMSchemaService;
+import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
+import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfOperationsService;
+import org.opendaylight.yang.gen.v1.module._1.rev140101.Module1Data;
+import org.opendaylight.yang.gen.v1.module._2.rev140102.Module2Data;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
 
 @RunWith(MockitoJUnitRunner.StrictStubs.class)
 public class RestconfOperationsServiceTest {
+    private static final String DEVICE_ID = "network-topology:network-topology/topology=topology-netconf/"
+        + "node=device/yang-ext:mount";
+    private static final String DEVICE_RPC1_MODULE1_ID = DEVICE_ID + "module1:dummy-rpc1-module1";
+    private static final String EXPECTED_JSON = """
+        {
+          "ietf-restconf:operations" : {
+            "module1:dummy-rpc1-module1": [null],
+            "module1:dummy-rpc2-module1": [null],
+            "module2:dummy-rpc1-module2": [null],
+            "module2:dummy-rpc2-module2": [null]
+          }
+        }""";
+    private static final String EXPECTED_XML = """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
+                    xmlns:ns0="module:1"
+                    xmlns:ns1="module:2" >
+          <ns0:dummy-rpc1-module1/>
+          <ns0:dummy-rpc2-module1/>
+          <ns1:dummy-rpc1-module2/>
+          <ns1:dummy-rpc2-module2/>
+        </operations>""";
+    private static RestconfOperationsService opService;
+
+    @BeforeClass
+    public static void startUp() {
+        final var runtimeContext = BindingRuntimeHelpers.createRuntimeContext(Module1Data.class, Module2Data.class,
+            NetworkTopology.class);
+        final var context = runtimeContext.getEffectiveModelContext();
+        final var mockMountPointService = mock(DOMMountPointService.class);
+        final var mockDomMountPoint = mock(DOMMountPoint.class);
+        final var mockDomSchemaService = mock(DOMSchemaService.class);
+        final var mockDataBroker = mock(DOMDataBroker.class);
+        doReturn(context).when(mockDomSchemaService).getGlobalContext();
+        doReturn(Optional.of(mockDomSchemaService)).when(mockDomMountPoint).getService(DOMSchemaService.class);
+        doReturn(Optional.of(mockDomMountPoint)).when(mockMountPointService).getMountPoint(any());
+        final var schemaContextHandler = new SchemaContextHandler(mockDataBroker, mockDomSchemaService);
+        schemaContextHandler.onModelContextUpdated(context);
+        opService = new RestconfOperationsServiceImpl(schemaContextHandler, mockMountPointService);
+    }
+
+    @Test
+    public void testOperationsJson() {
+        final var operationsJSON = opService.getOperationsJSON();
+        assertEquals(EXPECTED_JSON, operationsJSON);
+    }
+
+    @Test
+    public void testOperationsXml() {
+        final var operationsXML = opService.getOperationsXML();
+        assertEquals(EXPECTED_XML, operationsXML);
+    }
+
+    @Test
+    public void testMountPointOperationsJson() {
+        final var operationJSON = opService.getOperationJSON(DEVICE_ID);
+        assertEquals(EXPECTED_JSON, operationJSON);
+    }
+
+    @Test
+    public void testMountPointOperationsXml() {
+        final var operationXML = opService.getOperationXML(DEVICE_ID);
+        assertEquals(EXPECTED_XML, operationXML);
+    }
+
+    @Test
+    public void testMountPointSpecificOperationsJson() {
+        final var operationJSON = opService.getOperationJSON(DEVICE_RPC1_MODULE1_ID);
+        assertEquals("""
+            {
+              "ietf-restconf:operations" : {
+                "module1:dummy-rpc1-module1": [null]
+              }
+            }""", operationJSON);
+    }
+
     @Test
-    public void getOperationsTest() throws IOException {
-        final var oper = new RestconfOperationsServiceImpl(
-            TestUtils.newSchemaContextHandler(
-                YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/modules"))),
-            mock(DOMMountPointService.class));
-
-        assertEquals("{\n"
-            + "  \"ietf-restconf:operations\" : {\n"
-            + "    \"module1:dummy-rpc1-module1\": [null],\n"
-            + "    \"module1:dummy-rpc2-module1\": [null],\n"
-            + "    \"module2:dummy-rpc1-module2\": [null],\n"
-            + "    \"module2:dummy-rpc2-module2\": [null]\n"
-            + "  }\n"
-            + "}", oper.getOperationsJSON());
-        assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-            + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"\n"
-            + "            xmlns:ns0=\"module:1\"\n"
-            + "            xmlns:ns1=\"module:2\" >\n"
-            + "  <ns0:dummy-rpc1-module1/>\n"
-            + "  <ns0:dummy-rpc2-module1/>\n"
-            + "  <ns1:dummy-rpc1-module2/>\n"
-            + "  <ns1:dummy-rpc2-module2/>\n"
-            + "</operations>", oper.getOperationsXML());
+    public void testMountPointSpecificOperationsXml() {
+        final var operationXML = opService.getOperationXML(DEVICE_RPC1_MODULE1_ID);
+        assertEquals("""
+            <?xml version="1.0" encoding="UTF-8"?>
+            <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
+                        xmlns:ns0="module:1" >
+              <ns0:dummy-rpc1-module1/>
+            </operations>""", operationXML);
     }
 }
index 386c802329ac598ac0ca7ce24c8b1d8e2ce5a67c..d8c097ff9c17abb3434b3ca814309e6459b2fcf2 100644 (file)
@@ -4,4 +4,5 @@ module foo {
   revision 2021-09-30;
 
   rpc new;
+  rpc new1;
 }