Unit tests for Cluster Metadata Manager

Change-Id: I81de294844414778798645b95b5aaa18060e4c44
diff --git a/core/net/src/main/java/org/onosproject/cluster/impl/ConfigFileBasedClusterMetadataProvider.java b/core/net/src/main/java/org/onosproject/cluster/impl/ConfigFileBasedClusterMetadataProvider.java
index 3414033..cd21490 100644
--- a/core/net/src/main/java/org/onosproject/cluster/impl/ConfigFileBasedClusterMetadataProvider.java
+++ b/core/net/src/main/java/org/onosproject/cluster/impl/ConfigFileBasedClusterMetadataProvider.java
@@ -139,9 +139,11 @@
     @Override
     public void setClusterMetadata(ClusterMetadata metadata) {
         try {
-            Files.createParentDirs(CONFIG_FILE);
-            mapper.writeValue(CONFIG_FILE, metadata);
-            providerService.clusterMetadataChanged(new Versioned<>(metadata, CONFIG_FILE.lastModified()));
+            File configFile = new File(metadataUrl.replaceFirst("file://", ""));
+            Files.createParentDirs(configFile);
+            mapper.writeValue(configFile, metadata);
+            cachedMetadata.set(fetchMetadata(metadataUrl));
+            providerService.clusterMetadataChanged(new Versioned<>(metadata, configFile.lastModified()));
         } catch (IOException e) {
             Throwables.propagate(e);
         }
diff --git a/core/net/src/test/java/org/onosproject/cluster/impl/ClusterMetadataManagerTest.java b/core/net/src/test/java/org/onosproject/cluster/impl/ClusterMetadataManagerTest.java
new file mode 100644
index 0000000..08a6f9d
--- /dev/null
+++ b/core/net/src/test/java/org/onosproject/cluster/impl/ClusterMetadataManagerTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.cluster.impl;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.junit.HttpResourceUrlInterceptor;
+import org.onlab.junit.LoggerAdapter;
+import org.onlab.junit.TestUtils;
+import org.onosproject.cluster.ClusterMetadata;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.Partition;
+import org.onosproject.common.event.impl.TestEventDispatcher;
+import org.onosproject.core.VersionServiceAdapter;
+import org.slf4j.helpers.MessageFormatter;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Unit tests for the cluster metadata manager.
+ */
+
+public class ClusterMetadataManagerTest {
+
+    private static final String CLUSTER_NAME = "MyCluster";
+    private static final String NODE_ID = "MyId";
+    private static final String NODE_IP = "11.22.33.44";
+    private static final int NODE_PORT = 4523;
+
+    private static final String CLUSTER_METADATA_FORMAT =
+            "{\"nodes\": [" +
+            "   {\"ip\": \"{}\", \"id\": \"{}\", \"port\": {}}]," +
+                "\"name\": \"{}\"," +
+                "\"partitions\": [{\"id\": 1, \"members\": [\"{}\"]}]}";
+
+    private static final String CLUSTER_METADATA =
+            format(CLUSTER_METADATA_FORMAT, NODE_IP, NODE_ID, NODE_PORT, CLUSTER_NAME, NODE_ID);
+
+    private ClusterMetadataManager mgr;
+    private ConfigFileBasedClusterMetadataProvider fileProvider;
+    private DefaultClusterMetadataProvider defaultProvider;
+
+    private static String format(String format, Object... params) {
+        return MessageFormatter.arrayFormat(format, params).getMessage();
+    }
+
+    private class TrackingLogger extends LoggerAdapter {
+        int errors = 0;
+        StringBuilder messages = new StringBuilder();
+
+        public final void error(String msg, Throwable t) {
+            messages.append(msg);
+            messages.append(t);
+            errors++;
+        }
+    }
+
+    @Before
+    public void setUp() {
+        System.clearProperty("onos.cluster.metadata.uri");
+        mgr = new ClusterMetadataManager();
+        TestUtils.setField(mgr, "eventDispatcher", new TestEventDispatcher());
+        fileProvider = new ConfigFileBasedClusterMetadataProvider();
+        fileProvider.providerRegistry = mgr;
+
+        defaultProvider = new DefaultClusterMetadataProvider();
+        defaultProvider.providerRegistry = mgr;
+        defaultProvider.versionService = new VersionServiceAdapter();
+
+        mgr.activate();
+        defaultProvider.activate();
+
+        pause(200);
+    }
+
+    @After
+    public void tearDown() {
+        defaultProvider.deactivate();
+        fileProvider.deactivate();
+        mgr.deactivate();
+    }
+
+    private void pause(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException ie) {
+            throw new IllegalStateException(ie);
+        }
+    }
+
+    /**
+     * Tests that the file provider functions correctly when no metadata is present.
+     */
+    @Test
+    public void testNoMetadata() {
+        fileProvider.activate();
+        TrackingLogger log = new TrackingLogger();
+        TestUtils.setField(fileProvider, "log", log);
+
+        assertThat(fileProvider.isAvailable(), is(false));
+
+        boolean exceptionSeen;
+        try {
+            fileProvider.getClusterMetadata();
+            exceptionSeen = false;
+        } catch (Throwable t) {
+            exceptionSeen = true;
+        }
+        assertThat(exceptionSeen, is(true));
+
+        assertThat("Logger errors detected: " + log.messages.toString(), log.errors, is(0));
+    }
+
+    /**
+     * Tests the default provider.
+     */
+    @Test
+    public void testDefaultProvider() {
+        fileProvider.activate();
+        ClusterMetadata metadata = mgr.getClusterMetadata();
+
+        assertThat(metadata, notNullValue());
+        assertThat(metadata.getName(), is(defaultProvider.id().scheme()));
+        assertThat(metadata.getNodes(), hasSize(1));
+        assertThat(metadata.getPartitions(), hasSize(1));
+
+        String localAddress = mgr.getLocalNode().ip().toString();
+
+        ControllerNode node = metadata.getNodes().iterator().next();
+        assertThat(node.ip().toString(), is(localAddress));
+        assertThat(node.id().id(), is(localAddress));
+
+        Partition partition = metadata.getPartitions().iterator().next();
+        assertThat(partition.getId().asInt(), is(1));
+        assertThat(partition.getMembers(), hasSize(1));
+        assertThat(partition.getMembers().iterator().next().id(), is(localAddress));
+    }
+
+    /**
+     * Tests the file based cluster metadata provider.
+     */
+    @Test
+    public void testFileBasedProvider() {
+        File jsonFile;
+
+        try {
+            jsonFile = File.createTempFile("cluster", "json");
+            FileUtils.writeStringToFile(jsonFile, CLUSTER_METADATA);
+        } catch (IOException ioe) {
+            throw new IllegalStateException(ioe);
+        }
+
+        System.setProperty("onos.cluster.metadata.uri", "file://" + jsonFile.getAbsolutePath());
+        fileProvider.activate();
+
+        ClusterMetadata metadata = fileProvider.getClusterMetadata().value();
+        assertThat(metadata, notNullValue());
+
+        assertThat(metadata.getName(), is(CLUSTER_NAME));
+
+        ControllerNode node = metadata.getNodes().iterator().next();
+        assertThat(node.ip().toString(), is(NODE_IP));
+        assertThat(node.id().id(), is(NODE_ID));
+
+        Partition partition = metadata.getPartitions().iterator().next();
+        assertThat(partition.getId().asInt(), is(1));
+        assertThat(partition.getMembers(), hasSize(1));
+        assertThat(partition.getMembers().iterator().next().id(), is(NODE_ID));
+
+        ClusterMetadata newMetadata = new ClusterMetadata(metadata.providerId(),
+                "NewMetadata",
+                ImmutableSet.of(node),
+                ImmutableSet.of(partition));
+        mgr.setClusterMetadata(newMetadata);
+
+        assertThat(fileProvider.getClusterMetadata().value(), equalTo(newMetadata));
+
+        assertThat(jsonFile.delete(), is(true));
+    }
+
+    /**
+     * Tests fetching metadata from an HTTP source.
+     */
+    @Test
+    public void testUrlFetch() {
+        URL.setURLStreamHandlerFactory(
+                new HttpResourceUrlInterceptor.HttpResourceUrlInterceptorFactory("cluster-info.json"));
+        System.setProperty("onos.cluster.metadata.uri",  "http://opennetworking.org");
+
+        fileProvider.activate();
+        pause(400);
+
+
+        ClusterMetadata metadata = fileProvider.getClusterMetadata().value();
+        assertThat(metadata, notNullValue());
+
+        assertThat(metadata.getName(), is(CLUSTER_NAME));
+        assertThat(metadata.getNodes(), hasSize(1));
+        assertThat(metadata.getPartitions(), hasSize(1));
+    }
+}
\ No newline at end of file
diff --git a/utils/junit/BUCK b/utils/junit/BUCK
index c2fea21..cf8c29a 100644
--- a/utils/junit/BUCK
+++ b/utils/junit/BUCK
@@ -2,6 +2,7 @@
     '//lib:junit',
     '//lib:hamcrest-all',
     '//lib:guava',
+    '//lib:slf4j-api',
 ]
 
 TEST_DEPS = [
diff --git a/utils/junit/src/main/java/org/onlab/junit/HttpResourceUrlInterceptor.java b/utils/junit/src/main/java/org/onlab/junit/HttpResourceUrlInterceptor.java
new file mode 100644
index 0000000..045fd4c
--- /dev/null
+++ b/utils/junit/src/main/java/org/onlab/junit/HttpResourceUrlInterceptor.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onlab.junit;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+
+/**
+ * Intercepts HTTP URL connections and supplies predefined data from a resource. Used for supplying data to HTTP
+ * connections in unit tests.
+ */
+public class HttpResourceUrlInterceptor {
+
+    /**
+     * Handles creation of HTTP Connections to the resource data.
+     */
+    private static class HttpResourceUrlInterceptorHandler extends URLStreamHandler {
+
+        String resourceName;
+
+        HttpResourceUrlInterceptorHandler(String resourceName) {
+            this.resourceName = resourceName;
+        }
+
+        @Override
+        protected URLConnection openConnection(URL u) throws IOException {
+            return new InterceptedHttpUrlConnection(u, resourceName);
+        }
+    }
+
+    /**
+     * Creates stream handlers for the interceptor.
+     */
+    public static class HttpResourceUrlInterceptorFactory implements URLStreamHandlerFactory {
+
+        String resourceName;
+
+        public HttpResourceUrlInterceptorFactory(String resourceName) {
+            this.resourceName = resourceName;
+        }
+
+        @Override
+        public URLStreamHandler createURLStreamHandler(String protocol) {
+            return new HttpResourceUrlInterceptorHandler(resourceName);
+        }
+    }
+
+    /**
+     * HTTP Url Connection that is backed by the data in the resource.
+     */
+    private static final class InterceptedHttpUrlConnection extends HttpURLConnection {
+
+        private final String resourceName;
+
+        private InterceptedHttpUrlConnection(URL url, String resourceName) {
+            super(url);
+            this.resourceName = resourceName;
+        }
+
+        @Override
+        public int getResponseCode() {
+            return HTTP_OK;
+        }
+
+        @Override
+        public boolean usingProxy() {
+            return false;
+        }
+
+        @Override
+        public void disconnect() {
+            // noop
+        }
+
+        @Override
+        public void connect() {
+            // noop
+        }
+
+        @Override
+        public InputStream getInputStream() throws IOException {
+            return this.getClass().getResource(resourceName).openStream();
+        }
+
+    }
+}
+
+
diff --git a/utils/junit/src/main/java/org/onlab/junit/LoggerAdapter.java b/utils/junit/src/main/java/org/onlab/junit/LoggerAdapter.java
new file mode 100644
index 0000000..5e4f635e
--- /dev/null
+++ b/utils/junit/src/main/java/org/onlab/junit/LoggerAdapter.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onlab.junit;
+
+import org.slf4j.Logger;
+import org.slf4j.Marker;
+
+public class LoggerAdapter implements Logger {
+    @Override
+    public String getName() {
+        return "Adapter";
+    }
+
+    @Override
+    public boolean isTraceEnabled() {
+        return false;
+    }
+
+    @Override
+    public void trace(String s) {
+
+    }
+
+    @Override
+    public void trace(String s, Object o) {
+
+    }
+
+    @Override
+    public void trace(String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void trace(String s, Object... objects) {
+
+    }
+
+    @Override
+    public void trace(String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isTraceEnabled(Marker marker) {
+        return false;
+    }
+
+    @Override
+    public void trace(Marker marker, String s) {
+
+    }
+
+    @Override
+    public void trace(Marker marker, String s, Object o) {
+
+    }
+
+    @Override
+    public void trace(Marker marker, String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void trace(Marker marker, String s, Object... objects) {
+
+    }
+
+    @Override
+    public void trace(Marker marker, String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isDebugEnabled() {
+        return false;
+    }
+
+    @Override
+    public void debug(String s) {
+
+    }
+
+    @Override
+    public void debug(String s, Object o) {
+
+    }
+
+    @Override
+    public void debug(String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void debug(String s, Object... objects) {
+
+    }
+
+    @Override
+    public void debug(String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isDebugEnabled(Marker marker) {
+        return false;
+    }
+
+    @Override
+    public void debug(Marker marker, String s) {
+
+    }
+
+    @Override
+    public void debug(Marker marker, String s, Object o) {
+
+    }
+
+    @Override
+    public void debug(Marker marker, String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void debug(Marker marker, String s, Object... objects) {
+
+    }
+
+    @Override
+    public void debug(Marker marker, String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isInfoEnabled() {
+        return false;
+    }
+
+    @Override
+    public void info(String s) {
+
+    }
+
+    @Override
+    public void info(String s, Object o) {
+
+    }
+
+    @Override
+    public void info(String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void info(String s, Object... objects) {
+
+    }
+
+    @Override
+    public void info(String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isInfoEnabled(Marker marker) {
+        return false;
+    }
+
+    @Override
+    public void info(Marker marker, String s) {
+
+    }
+
+    @Override
+    public void info(Marker marker, String s, Object o) {
+
+    }
+
+    @Override
+    public void info(Marker marker, String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void info(Marker marker, String s, Object... objects) {
+
+    }
+
+    @Override
+    public void info(Marker marker, String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isWarnEnabled() {
+        return false;
+    }
+
+    @Override
+    public void warn(String s) {
+
+    }
+
+    @Override
+    public void warn(String s, Object o) {
+
+    }
+
+    @Override
+    public void warn(String s, Object... objects) {
+
+    }
+
+    @Override
+    public void warn(String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void warn(String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isWarnEnabled(Marker marker) {
+        return false;
+    }
+
+    @Override
+    public void warn(Marker marker, String s) {
+
+    }
+
+    @Override
+    public void warn(Marker marker, String s, Object o) {
+
+    }
+
+    @Override
+    public void warn(Marker marker, String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void warn(Marker marker, String s, Object... objects) {
+
+    }
+
+    @Override
+    public void warn(Marker marker, String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isErrorEnabled() {
+        return false;
+    }
+
+    @Override
+    public void error(String s) {
+
+    }
+
+    @Override
+    public void error(String s, Object o) {
+
+    }
+
+    @Override
+    public void error(String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void error(String s, Object... objects) {
+
+    }
+
+    @Override
+    public void error(String s, Throwable throwable) {
+
+    }
+
+    @Override
+    public boolean isErrorEnabled(Marker marker) {
+        return false;
+    }
+
+    @Override
+    public void error(Marker marker, String s) {
+
+    }
+
+    @Override
+    public void error(Marker marker, String s, Object o) {
+
+    }
+
+    @Override
+    public void error(Marker marker, String s, Object o, Object o1) {
+
+    }
+
+    @Override
+    public void error(Marker marker, String s, Object... objects) {
+
+    }
+
+    @Override
+    public void error(Marker marker, String s, Throwable throwable) {
+
+    }
+}
diff --git a/utils/junit/src/main/resources/org/onlab/junit/cluster-info.json b/utils/junit/src/main/resources/org/onlab/junit/cluster-info.json
new file mode 100644
index 0000000..152635b
--- /dev/null
+++ b/utils/junit/src/main/resources/org/onlab/junit/cluster-info.json
@@ -0,0 +1,18 @@
+{
+  "nodes": [
+    {
+      "ip": "10.128.11.141",
+      "id": "10.128.11.141",
+      "port": 9876
+    }
+  ],
+  "name": "MyCluster",
+  "partitions": [
+    {
+      "id": 1,
+      "members": [
+        "10.128.11.141"
+      ]
+    }
+  ]
+}