/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.nifi.fingerprint; import static org.apache.nifi.fingerprint.FingerprintFactory.FLOW_CONFIG_XSD; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.IOException; import java.lang.reflect.Method; import java.util.Collections; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.connectable.Position; import org.apache.nifi.controller.ScheduledState; import org.apache.nifi.controller.serialization.FlowSerializer; import org.apache.nifi.controller.serialization.StandardFlowSerializer; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.groups.RemoteProcessGroup; import org.apache.nifi.remote.RemoteGroupPort; import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol; import org.apache.nifi.util.NiFiProperties; import org.junit.Before; import org.junit.Test; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; /** */ public class FingerprintFactoryTest { private NiFiProperties nifiProperties; private StringEncryptor encryptor; private FingerprintFactory fingerprinter; @Before public void setup() { nifiProperties = getNiFiProperties(); encryptor = StringEncryptor.createEncryptor(nifiProperties); fingerprinter = new FingerprintFactory(encryptor); } @Test public void testSameFingerprint() throws IOException { final String fp1 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow1a.xml"), null); final String fp2 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow1b.xml"), null); assertEquals(fp1, fp2); } @Test public void testDifferentFingerprint() throws IOException { final String fp1 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow1a.xml"), null); final String fp2 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow2.xml"), null); assertNotEquals(fp1, fp2); } @Test public void testResourceValueInFingerprint() throws IOException { final String fingerprint = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow1a.xml"), null); assertEquals(3, StringUtils.countMatches(fingerprint, "success")); assertTrue(fingerprint.contains("In Connection")); } @Test public void testSameFlowWithDifferentBundleShouldHaveDifferentFingerprints() throws IOException { final String fp1 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow3-with-bundle-1.xml"), null); assertTrue(fp1.contains("org.apache.nifinifi-standard-nar1.0")); final String fp2 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow3-with-bundle-2.xml"), null); assertTrue(fp2.contains("org.apache.nifinifi-standard-nar2.0")); assertNotEquals(fp1, fp2); } @Test public void testSameFlowAndOneHasNoBundleShouldHaveDifferentFingerprints() throws IOException { final String fp1 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow3-with-bundle-1.xml"), null); assertTrue(fp1.contains("org.apache.nifinifi-standard-nar1.0")); final String fp2 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow3-with-no-bundle.xml"), null); assertTrue(fp2.contains("MISSING_BUNDLE")); assertNotEquals(fp1, fp2); } @Test public void testSameFlowAndOneHasMissingBundleShouldHaveDifferentFingerprints() throws IOException { final String fp1 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow3-with-bundle-1.xml"), null); assertTrue(fp1.contains("org.apache.nifinifi-standard-nar1.0")); final String fp2 = fingerprinter.createFingerprint(getResourceBytes("/nifi/fingerprint/flow3-with-missing-bundle.xml"), null); assertTrue(fp2.contains("missingmissingmissing")); assertNotEquals(fp1, fp2); } @Test public void testSchemaValidation() throws IOException { FingerprintFactory fp = new FingerprintFactory(null, getValidatingDocumentBuilder()); final String fingerprint = fp.createFingerprint(getResourceBytes("/nifi/fingerprint/validating-flow.xml"), null); } private byte[] getResourceBytes(final String resource) throws IOException { return IOUtils.toByteArray(FingerprintFactoryTest.class.getResourceAsStream(resource)); } private DocumentBuilder getValidatingDocumentBuilder() { final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); final Schema schema; try { schema = schemaFactory.newSchema(FingerprintFactory.class.getResource(FLOW_CONFIG_XSD)); } catch (final Exception e) { throw new RuntimeException("Failed to parse schema for file flow configuration.", e); } try { documentBuilderFactory.setSchema(schema); DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); docBuilder.setErrorHandler(new ErrorHandler() { @Override public void warning(SAXParseException e) throws SAXException { throw e; } @Override public void error(SAXParseException e) throws SAXException { throw e; } @Override public void fatalError(SAXParseException e) throws SAXException { throw e; } }); return docBuilder; } catch (final Exception e) { throw new RuntimeException("Failed to create document builder for flow configuration.", e); } } private <T> Element serializeElement(final StringEncryptor encryptor, final Class<T> componentClass, final T component, final String serializerMethodName) throws Exception { final DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); final DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); final Document doc = docBuilder.newDocument(); final FlowSerializer flowSerializer = new StandardFlowSerializer(encryptor); final Method serializeMethod = StandardFlowSerializer.class.getDeclaredMethod(serializerMethodName, Element.class, componentClass); serializeMethod.setAccessible(true); final Element rootElement = doc.createElement("root"); serializeMethod.invoke(flowSerializer, rootElement, component); return rootElement; } private NiFiProperties getNiFiProperties() { final NiFiProperties nifiProperties = mock(NiFiProperties.class); when(nifiProperties.getProperty(StringEncryptor.NF_SENSITIVE_PROPS_ALGORITHM)).thenReturn("PBEWITHMD5AND256BITAES-CBC-OPENSSL"); when(nifiProperties.getProperty(StringEncryptor.NF_SENSITIVE_PROPS_PROVIDER)).thenReturn("BC"); when(nifiProperties.getProperty(anyString(), anyString())).then(invocation -> invocation.getArgumentAt(1, String.class)); return nifiProperties; } private <T> String fingerprint(final String methodName, final Class<T> inputClass, final T input) throws Exception { final Method fingerprintFromComponent = FingerprintFactory.class.getDeclaredMethod(methodName, StringBuilder.class, inputClass); fingerprintFromComponent.setAccessible(true); final StringBuilder fingerprint = new StringBuilder(); fingerprintFromComponent.invoke(fingerprinter, fingerprint, input); return fingerprint.toString(); } @Test public void testRemoteProcessGroupFingerprintRaw() throws Exception { // Fill out every configuration. final RemoteProcessGroup component = mock(RemoteProcessGroup.class); when(component.getName()).thenReturn("name"); when(component.getIdentifier()).thenReturn("id"); when(component.getPosition()).thenReturn(new Position(10.5, 20.3)); when(component.getTargetUri()).thenReturn("http://node1:8080/nifi"); when(component.getTargetUris()).thenReturn("http://node1:8080/nifi, http://node2:8080/nifi"); when(component.getNetworkInterface()).thenReturn("eth0"); when(component.getComments()).thenReturn("comment"); when(component.getCommunicationsTimeout()).thenReturn("10 sec"); when(component.getYieldDuration()).thenReturn("30 sec"); when(component.getTransportProtocol()).thenReturn(SiteToSiteTransportProtocol.RAW); when(component.getProxyHost()).thenReturn(null); when(component.getProxyPort()).thenReturn(null); when(component.getProxyUser()).thenReturn(null); when(component.getProxyPassword()).thenReturn(null); // Assert fingerprints with expected one. final String expected = "id" + "http://node1:8080/nifi, http://node2:8080/nifi" + "eth0" + "10 sec" + "30 sec" + "RAW" + "NO_VALUE" + "NO_VALUE" + "NO_VALUE" + "NO_VALUE"; final Element rootElement = serializeElement(encryptor, RemoteProcessGroup.class, component, "addRemoteProcessGroup"); final Element componentElement = (Element) rootElement.getElementsByTagName("remoteProcessGroup").item(0); assertEquals(expected, fingerprint("addRemoteProcessGroupFingerprint", Element.class, componentElement)); } @Test public void testRemoteProcessGroupFingerprintWithProxy() throws Exception { // Fill out every configuration. final RemoteProcessGroup component = mock(RemoteProcessGroup.class); when(component.getName()).thenReturn("name"); when(component.getIdentifier()).thenReturn("id"); when(component.getPosition()).thenReturn(new Position(10.5, 20.3)); when(component.getTargetUri()).thenReturn("http://node1:8080/nifi"); when(component.getTargetUris()).thenReturn("http://node1:8080/nifi, http://node2:8080/nifi"); when(component.getComments()).thenReturn("comment"); when(component.getCommunicationsTimeout()).thenReturn("10 sec"); when(component.getYieldDuration()).thenReturn("30 sec"); when(component.getTransportProtocol()).thenReturn(SiteToSiteTransportProtocol.HTTP); when(component.getProxyHost()).thenReturn("proxy-host"); when(component.getProxyPort()).thenReturn(3128); when(component.getProxyUser()).thenReturn("proxy-user"); when(component.getProxyPassword()).thenReturn("proxy-pass"); // Assert fingerprints with expected one. final String expected = "id" + "http://node1:8080/nifi, http://node2:8080/nifi" + "NO_VALUE" + "10 sec" + "30 sec" + "HTTP" + "proxy-host" + "3128" + "proxy-user" + "proxy-pass"; final Element rootElement = serializeElement(encryptor, RemoteProcessGroup.class, component, "addRemoteProcessGroup"); final Element componentElement = (Element) rootElement.getElementsByTagName("remoteProcessGroup").item(0); assertEquals(expected.toString(), fingerprint("addRemoteProcessGroupFingerprint", Element.class, componentElement)); } @Test public void testRemotePortFingerprint() throws Exception { // Fill out every configuration. final RemoteProcessGroup groupComponent = mock(RemoteProcessGroup.class); when(groupComponent.getName()).thenReturn("name"); when(groupComponent.getIdentifier()).thenReturn("id"); when(groupComponent.getPosition()).thenReturn(new Position(10.5, 20.3)); when(groupComponent.getTargetUri()).thenReturn("http://node1:8080/nifi"); when(groupComponent.getTransportProtocol()).thenReturn(SiteToSiteTransportProtocol.RAW); final RemoteGroupPort portComponent = mock(RemoteGroupPort.class); when(groupComponent.getInputPorts()).thenReturn(Collections.singleton(portComponent)); when(portComponent.getName()).thenReturn("portName"); when(portComponent.getIdentifier()).thenReturn("portId"); when(portComponent.getPosition()).thenReturn(new Position(10.5, 20.3)); when(portComponent.getComments()).thenReturn("portComment"); when(portComponent.getScheduledState()).thenReturn(ScheduledState.RUNNING); when(portComponent.getMaxConcurrentTasks()).thenReturn(3); when(portComponent.isUseCompression()).thenReturn(true); when(portComponent.getBatchCount()).thenReturn(1234); when(portComponent.getBatchSize()).thenReturn("64KB"); when(portComponent.getBatchDuration()).thenReturn("10sec"); // Serializer doesn't serialize if a port doesn't have any connection. when(portComponent.hasIncomingConnection()).thenReturn(true); // Assert fingerprints with expected one. final String expected = "portId" + "3" + "true" + "1234" + "64KB" + "10sec"; final Element rootElement = serializeElement(encryptor, RemoteProcessGroup.class, groupComponent, "addRemoteProcessGroup"); final Element componentElement = (Element) rootElement.getElementsByTagName("inputPort").item(0); assertEquals(expected.toString(), fingerprint("addRemoteGroupPortFingerprint", Element.class, componentElement)); } }