Merge "Add mdns files and unit tests"

This commit is contained in:
Paul Hu
2022-06-28 05:43:58 +00:00
committed by Gerrit Code Review
38 changed files with 7249 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkRequest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Tests for {@link ConnectivityMonitor}. */
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class ConnectivityMonitorWithConnectivityManagerTests {
@Mock private Context mContext;
@Mock private ConnectivityMonitor.Listener mockListener;
@Mock private ConnectivityManager mConnectivityManager;
private ConnectivityMonitorWithConnectivityManager monitor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
doReturn(mConnectivityManager).when(mContext)
.getSystemService(Context.CONNECTIVITY_SERVICE);
monitor = new ConnectivityMonitorWithConnectivityManager(mContext, mockListener);
}
@Test
public void testInitialState_shouldNotRegisterNetworkCallback() {
verifyNetworkCallbackRegistered(0 /* time */);
verifyNetworkCallbackUnregistered(0 /* time */);
}
@Test
public void testStartDiscovery_shouldRegisterNetworkCallback() {
monitor.startWatchingConnectivityChanges();
verifyNetworkCallbackRegistered(1 /* time */);
verifyNetworkCallbackUnregistered(0 /* time */);
}
@Test
public void testStartDiscoveryTwice_shouldRegisterOneNetworkCallback() {
monitor.startWatchingConnectivityChanges();
monitor.startWatchingConnectivityChanges();
verifyNetworkCallbackRegistered(1 /* time */);
verifyNetworkCallbackUnregistered(0 /* time */);
}
@Test
public void testStopDiscovery_shouldUnregisterNetworkCallback() {
monitor.startWatchingConnectivityChanges();
monitor.stopWatchingConnectivityChanges();
verifyNetworkCallbackRegistered(1 /* time */);
verifyNetworkCallbackUnregistered(1 /* time */);
}
@Test
public void testStopDiscoveryTwice_shouldUnregisterNetworkCallback() {
monitor.startWatchingConnectivityChanges();
monitor.stopWatchingConnectivityChanges();
verifyNetworkCallbackRegistered(1 /* time */);
verifyNetworkCallbackUnregistered(1 /* time */);
}
@Test
public void testIntentFired_shouldNotifyListener() {
InOrder inOrder = inOrder(mockListener);
monitor.startWatchingConnectivityChanges();
final ArgumentCaptor<NetworkCallback> callbackCaptor =
ArgumentCaptor.forClass(NetworkCallback.class);
verify(mConnectivityManager, times(1)).registerNetworkCallback(
any(NetworkRequest.class), callbackCaptor.capture());
final NetworkCallback callback = callbackCaptor.getValue();
final Network testNetwork = new Network(1 /* netId */);
// Simulate network available.
callback.onAvailable(testNetwork);
inOrder.verify(mockListener).onConnectivityChanged();
// Simulate network lost.
callback.onLost(testNetwork);
inOrder.verify(mockListener).onConnectivityChanged();
// Simulate network unavailable.
callback.onUnavailable();
inOrder.verify(mockListener).onConnectivityChanged();
}
private void verifyNetworkCallbackRegistered(int time) {
verify(mConnectivityManager, times(time)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class));
}
private void verifyNetworkCallbackUnregistered(int time) {
verify(mConnectivityManager, times(time))
.unregisterNetworkCallback(any(NetworkCallback.class));
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.text.TextUtils;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
import java.util.Collections;
/** Tests for {@link MdnsDiscoveryManager}. */
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsDiscoveryManagerTests {
private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
private static final String SERVICE_TYPE_2 = "_test._tcp.local";
@Mock private ExecutorProvider executorProvider;
@Mock private MdnsSocketClient socketClient;
@Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
@Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
@Mock MdnsServiceBrowserListener mockListenerOne;
@Mock MdnsServiceBrowserListener mockListenerTwo;
private MdnsDiscoveryManager discoveryManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mockServiceTypeClientOne.getServiceTypeLabels())
.thenReturn(TextUtils.split(SERVICE_TYPE_1, "\\."));
when(mockServiceTypeClientTwo.getServiceTypeLabels())
.thenReturn(TextUtils.split(SERVICE_TYPE_2, "\\."));
discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient) {
@Override
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
if (serviceType.equals(SERVICE_TYPE_1)) {
return mockServiceTypeClientOne;
} else if (serviceType.equals(SERVICE_TYPE_2)) {
return mockServiceTypeClientTwo;
}
return null;
}
};
}
@Test
public void registerListener_unregisterListener() throws IOException {
discoveryManager.registerListener(
SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
verify(socketClient).startDiscovery();
verify(mockServiceTypeClientOne)
.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
verify(socketClient).stopDiscovery();
}
@Test
public void registerMultipleListeners() throws IOException {
discoveryManager.registerListener(
SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
verify(socketClient).startDiscovery();
verify(mockServiceTypeClientOne)
.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
discoveryManager.registerListener(
SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
verify(mockServiceTypeClientTwo)
.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
}
@Test
public void onResponseReceived() {
discoveryManager.registerListener(
SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
discoveryManager.registerListener(
SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
MdnsResponse responseForServiceTypeOne = createMockResponse(SERVICE_TYPE_1);
discoveryManager.onResponseReceived(responseForServiceTypeOne);
verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne);
MdnsResponse responseForServiceTypeTwo = createMockResponse(SERVICE_TYPE_2);
discoveryManager.onResponseReceived(responseForServiceTypeTwo);
verify(mockServiceTypeClientTwo).processResponse(responseForServiceTypeTwo);
MdnsResponse responseForSubtype = createMockResponse("subtype._sub._googlecast._tcp.local");
discoveryManager.onResponseReceived(responseForSubtype);
verify(mockServiceTypeClientOne).processResponse(responseForSubtype);
}
private MdnsResponse createMockResponse(String serviceType) {
MdnsPointerRecord mockPointerRecord = mock(MdnsPointerRecord.class);
MdnsResponse mockResponse = mock(MdnsResponse.class);
when(mockResponse.getPointerRecords())
.thenReturn(Collections.singletonList(mockPointerRecord));
when(mockPointerRecord.getName()).thenReturn(TextUtils.split(serviceType, "\\."));
return mockResponse;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.DatagramPacket;
import java.util.Locale;
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsPacketReaderTests {
@Test
public void testLimits() throws IOException {
byte[] data = new byte[25];
DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
// After creating a new reader, confirm that the remaining is equal to the packet length
// (or that there is no temporary limit).
MdnsPacketReader packetReader = new MdnsPacketReader(datagramPacket);
assertEquals(data.length, packetReader.getRemaining());
// Confirm that we can set the temporary limit to 0.
packetReader.setLimit(0);
assertEquals(0, packetReader.getRemaining());
// Confirm that we can clear the temporary limit, and restore to the length of the packet.
packetReader.clearLimit();
assertEquals(data.length, packetReader.getRemaining());
// Confirm that we can set the temporary limit to the actual length of the packet.
// While parsing packets, it is common to set the limit to the length of the packet.
packetReader.setLimit(data.length);
assertEquals(data.length, packetReader.getRemaining());
// Confirm that we ignore negative limits.
packetReader.setLimit(-10);
assertEquals(data.length, packetReader.getRemaining());
// Confirm that we can set the temporary limit to something less than the packet length.
packetReader.setLimit(data.length / 2);
assertEquals(data.length / 2, packetReader.getRemaining());
// Confirm that we throw an exception if trying to set the temporary limit beyond the
// packet length.
packetReader.clearLimit();
try {
packetReader.setLimit(data.length * 2 + 1);
fail("Should have thrown an IOException when trying to set the temporary limit beyond "
+ "the packet length");
} catch (IOException e) {
// Expected
} catch (Exception e) {
fail(String.format(
Locale.ROOT,
"Should not have thrown any other exception except " + "for IOException: %s",
e.getMessage()));
}
assertEquals(data.length, packetReader.getRemaining());
}
}

View File

@@ -0,0 +1,324 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import android.util.Log;
import com.android.net.module.util.HexDump;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.util.List;
// The record test data does not use compressed names (label pointers), since that would require
// additional data to populate the label dictionary accordingly.
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsRecordTests {
private static final String TAG = "MdnsRecordTests";
private static final int MAX_PACKET_SIZE = 4096;
private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
@Test
public void testInet4AddressRecord() throws IOException {
final byte[] dataIn = HexDump.hexStringToByteArray(
"0474657374000001" + "0001000011940004" + "0A010203");
assertNotNull(dataIn);
String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
// Decode
DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
assertNotNull(name);
assertEquals(1, name.length);
assertEquals("test", name[0]);
String fqdn = MdnsRecord.labelsToString(name);
assertEquals("test", fqdn);
int type = reader.readUInt16();
assertEquals(MdnsRecord.TYPE_A, type);
MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
Inet4Address addr = record.getInet4Address();
assertEquals("/10.1.2.3", addr.toString());
// Encode
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
byte[] dataOut = packet.getData();
String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
Log.d(TAG, dataOutText);
assertEquals(dataInText, dataOutText);
}
@Test
public void testTypeAAAInet6AddressRecord() throws IOException {
final byte[] dataIn = HexDump.hexStringToByteArray(
"047465737400001C"
+ "0001000011940010"
+ "AABBCCDD11223344"
+ "A0B0C0D010203040");
assertNotNull(dataIn);
String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
// Decode
DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
packet.setSocketAddress(
new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
assertNotNull(name);
assertEquals(1, name.length);
String fqdn = MdnsRecord.labelsToString(name);
assertEquals("test", fqdn);
int type = reader.readUInt16();
assertEquals(MdnsRecord.TYPE_AAAA, type);
MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
reader);
assertNull(record.getInet4Address());
Inet6Address addr = record.getInet6Address();
assertEquals("/aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040", addr.toString());
// Encode
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
packet = writer.getPacket(MULTICAST_IPV6_ADDRESS);
byte[] dataOut = packet.getData();
String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
Log.d(TAG, dataOutText);
assertEquals(dataInText, dataOutText);
}
@Test
public void testTypeAAAInet4AddressRecord() throws IOException {
final byte[] dataIn = HexDump.hexStringToByteArray(
"047465737400001C"
+ "0001000011940010"
+ "0000000000000000"
+ "0000FFFF10203040");
assertNotNull(dataIn);
HexDump.dumpHexString(dataIn, 0, dataIn.length);
// Decode
DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
packet.setSocketAddress(
new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
assertNotNull(name);
assertEquals(1, name.length);
String fqdn = MdnsRecord.labelsToString(name);
assertEquals("test", fqdn);
int type = reader.readUInt16();
assertEquals(MdnsRecord.TYPE_AAAA, type);
MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
reader);
assertNull(record.getInet6Address());
Inet4Address addr = record.getInet4Address();
assertEquals("/16.32.48.64", addr.toString());
// Encode
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
byte[] dataOut = packet.getData();
String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
Log.d(TAG, dataOutText);
final byte[] expectedDataIn =
HexDump.hexStringToByteArray("047465737400001C000100001194000410203040");
assertNotNull(expectedDataIn);
String expectedDataInText = HexDump.dumpHexString(expectedDataIn, 0, expectedDataIn.length);
assertEquals(expectedDataInText, dataOutText);
}
@Test
public void testPointerRecord() throws IOException {
final byte[] dataIn = HexDump.hexStringToByteArray(
"047465737400000C"
+ "000100001194000E"
+ "03666F6F03626172"
+ "047175787800");
assertNotNull(dataIn);
String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
// Decode
DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
assertNotNull(name);
assertEquals(1, name.length);
String fqdn = MdnsRecord.labelsToString(name);
assertEquals("test", fqdn);
int type = reader.readUInt16();
assertEquals(MdnsRecord.TYPE_PTR, type);
MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
String[] pointer = record.getPointer();
assertEquals("foo.bar.quxx", MdnsRecord.labelsToString(pointer));
assertFalse(record.hasSubtype());
assertNull(record.getSubtype());
// Encode
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
byte[] dataOut = packet.getData();
String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
Log.d(TAG, dataOutText);
assertEquals(dataInText, dataOutText);
}
@Test
public void testServiceRecord() throws IOException {
final byte[] dataIn = HexDump.hexStringToByteArray(
"0474657374000021"
+ "0001000011940014"
+ "000100FF1F480366"
+ "6F6F036261720471"
+ "75787800");
assertNotNull(dataIn);
String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
// Decode
DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
assertNotNull(name);
assertEquals(1, name.length);
String fqdn = MdnsRecord.labelsToString(name);
assertEquals("test", fqdn);
int type = reader.readUInt16();
assertEquals(MdnsRecord.TYPE_SRV, type);
MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
int servicePort = record.getServicePort();
assertEquals(8008, servicePort);
String serviceHost = MdnsRecord.labelsToString(record.getServiceHost());
assertEquals("foo.bar.quxx", serviceHost);
assertEquals(1, record.getServicePriority());
assertEquals(255, record.getServiceWeight());
// Encode
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
byte[] dataOut = packet.getData();
String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
Log.d(TAG, dataOutText);
assertEquals(dataInText, dataOutText);
}
@Test
public void testTextRecord() throws IOException {
final byte[] dataIn = HexDump.hexStringToByteArray(
"0474657374000010"
+ "0001000011940024"
+ "0D613D68656C6C6F"
+ "2074686572650C62"
+ "3D31323334353637"
+ "3839300878797A3D"
+ "21402324");
assertNotNull(dataIn);
String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
// Decode
DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
assertNotNull(name);
assertEquals(1, name.length);
String fqdn = MdnsRecord.labelsToString(name);
assertEquals("test", fqdn);
int type = reader.readUInt16();
assertEquals(MdnsRecord.TYPE_TXT, type);
MdnsTextRecord record = new MdnsTextRecord(name, reader);
List<String> strings = record.getStrings();
assertNotNull(strings);
assertEquals(3, strings.size());
assertEquals("a=hello there", strings.get(0));
assertEquals("b=1234567890", strings.get(1));
assertEquals("xyz=!@#$", strings.get(2));
// Encode
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
byte[] dataOut = packet.getData();
String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
Log.d(TAG, dataOutText);
assertEquals(dataInText, dataOutText);
}
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.server.connectivity.mdns.MdnsResponseDecoder.Clock;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import com.android.net.module.util.HexDump;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.util.LinkedList;
import java.util.List;
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsResponseDecoderTests {
private static final byte[] data = HexDump.hexStringToByteArray(
"0000840000000004"
+ "00000003134A6F68"
+ "6E6E792773204368"
+ "726F6D6563617374"
+ "0B5F676F6F676C65"
+ "63617374045F7463"
+ "70056C6F63616C00"
+ "0010800100001194"
+ "006C2369643D3937"
+ "3062663534376237"
+ "3533666336336332"
+ "6432613336626238"
+ "3936616261380576"
+ "653D30320D6D643D"
+ "4368726F6D656361"
+ "73741269633D2F73"
+ "657475702F69636F"
+ "6E2E706E6716666E"
+ "3D4A6F686E6E7927"
+ "73204368726F6D65"
+ "636173740463613D"
+ "350473743D30095F"
+ "7365727669636573"
+ "075F646E732D7364"
+ "045F756470C03100"
+ "0C00010000119400"
+ "02C020C020000C00"
+ "01000011940002C0"
+ "0CC00C0021800100"
+ "000078001C000000"
+ "001F49134A6F686E"
+ "6E79277320436872"
+ "6F6D6563617374C0"
+ "31C0F30001800100"
+ "0000780004C0A864"
+ "68C0F3002F800100"
+ "0000780005C0F300"
+ "0140C00C002F8001"
+ "000011940009C00C"
+ "00050000800040");
private static final byte[] data6 = HexDump.hexStringToByteArray(
"0000840000000001000000030B5F676F6F676C656361737404"
+ "5F746370056C6F63616C00000C000100000078003330476F6F676C"
+ "652D486F6D652D4D61782D61363836666331323961366638636265"
+ "31643636353139343065336164353766C00CC02E00108001000011"
+ "9400C02369643D6136383666633132396136663863626531643636"
+ "3531393430653361643537662363643D4133304233303032363546"
+ "36384341313233353532434639344141353742314613726D3D4335"
+ "35393134383530383841313638330576653D3035126D643D476F6F"
+ "676C6520486F6D65204D61781269633D2F73657475702F69636F6E"
+ "2E706E6710666E3D417474696320737065616B65720863613D3130"
+ "3234340473743D320F62733D464138464341363734453537046E66"
+ "3D320372733DC02E0021800100000078002D000000001F49246136"
+ "3836666331322D396136662D386362652D316436362D3531393430"
+ "65336164353766C01DC13F001C8001000000780010200033330000"
+ "0000DA6C63FFFE7C74830109018001000000780004C0A801026C6F"
+ "63616C0000018001000000780004C0A8010A000001800100000078"
+ "0004C0A8010A00000000000000");
private static final String DUMMY_CAST_SERVICE_NAME = "_googlecast";
private static final String[] DUMMY_CAST_SERVICE_TYPE =
new String[] {DUMMY_CAST_SERVICE_NAME, "_tcp", "local"};
private final List<MdnsResponse> responses = new LinkedList<>();
private final Clock mClock = mock(Clock.class);
@Before
public void setUp() {
MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
assertNotNull(data);
DatagramPacket packet = new DatagramPacket(data, data.length);
packet.setSocketAddress(
new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
responses.clear();
int errorCode = decoder.decode(packet, responses);
assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
assertEquals(1, responses.size());
}
@Test
public void testDecodeWithNullServiceType() {
MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
assertNotNull(data);
DatagramPacket packet = new DatagramPacket(data, data.length);
packet.setSocketAddress(
new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
responses.clear();
int errorCode = decoder.decode(packet, responses);
assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
assertEquals(2, responses.size());
}
@Test
public void testDecodeMultipleAnswerPacket() throws IOException {
MdnsResponse response = responses.get(0);
assertTrue(response.isComplete());
MdnsInetAddressRecord inet4AddressRecord = response.getInet4AddressRecord();
Inet4Address inet4Addr = inet4AddressRecord.getInet4Address();
assertNotNull(inet4Addr);
assertEquals("/192.168.100.104", inet4Addr.toString());
MdnsServiceRecord serviceRecord = response.getServiceRecord();
String serviceName = serviceRecord.getServiceName();
assertEquals(DUMMY_CAST_SERVICE_NAME, serviceName);
String serviceInstanceName = serviceRecord.getServiceInstanceName();
assertEquals("Johnny's Chromecast", serviceInstanceName);
String serviceHost = MdnsRecord.labelsToString(serviceRecord.getServiceHost());
assertEquals("Johnny's Chromecast.local", serviceHost);
int serviceProto = serviceRecord.getServiceProtocol();
assertEquals(MdnsServiceRecord.PROTO_TCP, serviceProto);
int servicePort = serviceRecord.getServicePort();
assertEquals(8009, servicePort);
int servicePriority = serviceRecord.getServicePriority();
assertEquals(0, servicePriority);
int serviceWeight = serviceRecord.getServiceWeight();
assertEquals(0, serviceWeight);
MdnsTextRecord textRecord = response.getTextRecord();
List<String> textStrings = textRecord.getStrings();
assertEquals(7, textStrings.size());
assertEquals("id=970bf547b753fc63c2d2a36bb896aba8", textStrings.get(0));
assertEquals("ve=02", textStrings.get(1));
assertEquals("md=Chromecast", textStrings.get(2));
assertEquals("ic=/setup/icon.png", textStrings.get(3));
assertEquals("fn=Johnny's Chromecast", textStrings.get(4));
assertEquals("ca=5", textStrings.get(5));
assertEquals("st=0", textStrings.get(6));
}
@Test
public void testDecodeIPv6AnswerPacket() throws IOException {
MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
assertNotNull(data6);
DatagramPacket packet = new DatagramPacket(data6, data6.length);
packet.setSocketAddress(
new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
responses.clear();
int errorCode = decoder.decode(packet, responses);
assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
MdnsResponse response = responses.get(0);
assertTrue(response.isComplete());
MdnsInetAddressRecord inet6AddressRecord = response.getInet6AddressRecord();
assertNotNull(inet6AddressRecord);
Inet4Address inet4Addr = inet6AddressRecord.getInet4Address();
assertNull(inet4Addr);
Inet6Address inet6Addr = inet6AddressRecord.getInet6Address();
assertNotNull(inet6Addr);
assertEquals(inet6Addr.getHostAddress(), "2000:3333::da6c:63ff:fe7c:7483");
}
@Test
public void testIsComplete() {
MdnsResponse response = responses.get(0);
assertTrue(response.isComplete());
response.clearPointerRecords();
assertFalse(response.isComplete());
response = responses.get(0);
response.setInet4AddressRecord(null);
assertFalse(response.isComplete());
response = responses.get(0);
response.setInet6AddressRecord(null);
assertFalse(response.isComplete());
response = responses.get(0);
response.setServiceRecord(null);
assertFalse(response.isComplete());
response = responses.get(0);
response.setTextRecord(null);
assertFalse(response.isComplete());
}
}

View File

@@ -0,0 +1,305 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.net.module.util.HexDump;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.DatagramPacket;
import java.util.Arrays;
import java.util.List;
// The record test data does not use compressed names (label pointers), since that would require
// additional data to populate the label dictionary accordingly.
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsResponseTests {
private static final String TAG = "MdnsResponseTests";
// MDNS response packet for name "test" with an IPv4 address of 10.1.2.3
private static final byte[] dataIn_ipv4_1 = HexDump.hexStringToByteArray(
"0474657374000001" + "0001000011940004" + "0A010203");
// MDNS response packet for name "tess" with an IPv4 address of 10.1.2.4
private static final byte[] dataIn_ipv4_2 = HexDump.hexStringToByteArray(
"0474657373000001" + "0001000011940004" + "0A010204");
// MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040
private static final byte[] dataIn_ipv6_1 = HexDump.hexStringToByteArray(
"047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203040");
// MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3030
private static final byte[] dataIn_ipv6_2 = HexDump.hexStringToByteArray(
"047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203030");
// MDNS response w/name "test" & PTR to foo.bar.quxx
private static final byte[] dataIn_ptr_1 = HexDump.hexStringToByteArray(
"047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787800");
// MDNS response w/name "test" & PTR to foo.bar.quxy
private static final byte[] dataIn_ptr_2 = HexDump.hexStringToByteArray(
"047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787900");
// MDNS response w/name "test" & Service for host foo.bar.quxx
private static final byte[] dataIn_service_1 = HexDump.hexStringToByteArray(
"0474657374000021"
+ "0001000011940014"
+ "000100FF1F480366"
+ "6F6F036261720471"
+ "75787800");
// MDNS response w/name "test" & Service for host test
private static final byte[] dataIn_service_2 = HexDump.hexStringToByteArray(
"0474657374000021" + "000100001194000B" + "000100FF1F480474" + "657374");
// MDNS response w/name "test" & the following text strings:
// "a=hello there", "b=1234567890", and "xyz=!$$$"
private static final byte[] dataIn_text_1 = HexDump.hexStringToByteArray(
"0474657374000010"
+ "0001000011940024"
+ "0D613D68656C6C6F"
+ "2074686572650C62"
+ "3D31323334353637"
+ "3839300878797A3D"
+ "21242424");
// MDNS response w/name "test" & the following text strings:
// "a=hello there", "b=1234567890", and "xyz=!@#$"
private static final byte[] dataIn_text_2 = HexDump.hexStringToByteArray(
"0474657374000010"
+ "0001000011940024"
+ "0D613D68656C6C6F"
+ "2074686572650C62"
+ "3D31323334353637"
+ "3839300878797A3D"
+ "21402324");
// The following helper classes act as wrappers so that IPv4 and IPv6 address records can
// be explicitly created by type using same constructor signature as all other records.
static class MdnsInet4AddressRecord extends MdnsInetAddressRecord {
public MdnsInet4AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
super(name, MdnsRecord.TYPE_A, reader);
}
}
static class MdnsInet6AddressRecord extends MdnsInetAddressRecord {
public MdnsInet6AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
super(name, MdnsRecord.TYPE_AAAA, reader);
}
}
// This helper class just wraps the data bytes of a response packet with the contained record
// type.
// Its only purpose is to make the test code a bit more readable.
static class PacketAndRecordClass {
public final byte[] packetData;
public final Class<?> recordClass;
public PacketAndRecordClass() {
packetData = null;
recordClass = null;
}
public PacketAndRecordClass(byte[] data, Class<?> c) {
packetData = data;
recordClass = c;
}
}
// Construct an MdnsResponse with the specified data packets applied.
private MdnsResponse makeMdnsResponse(long time, List<PacketAndRecordClass> responseList)
throws IOException {
MdnsResponse response = new MdnsResponse(time);
for (PacketAndRecordClass responseData : responseList) {
DatagramPacket packet =
new DatagramPacket(responseData.packetData, responseData.packetData.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
reader.skip(2); // skip record type indication.
// Apply the right kind of record to the response.
if (responseData.recordClass == MdnsInet4AddressRecord.class) {
response.setInet4AddressRecord(new MdnsInet4AddressRecord(name, reader));
} else if (responseData.recordClass == MdnsInet6AddressRecord.class) {
response.setInet6AddressRecord(new MdnsInet6AddressRecord(name, reader));
} else if (responseData.recordClass == MdnsPointerRecord.class) {
response.addPointerRecord(new MdnsPointerRecord(name, reader));
} else if (responseData.recordClass == MdnsServiceRecord.class) {
response.setServiceRecord(new MdnsServiceRecord(name, reader));
} else if (responseData.recordClass == MdnsTextRecord.class) {
response.setTextRecord(new MdnsTextRecord(name, reader));
} else {
fail("Unsupported/unexpected MdnsRecord subtype used in test - invalid test!");
}
}
return response;
}
@Test
public void getInet4AddressRecord_returnsAddedRecord() throws IOException {
DatagramPacket packet = new DatagramPacket(dataIn_ipv4_1, dataIn_ipv4_1.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
reader.skip(2); // skip record type indication.
MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
MdnsResponse response = new MdnsResponse(0);
assertFalse(response.hasInet4AddressRecord());
assertTrue(response.setInet4AddressRecord(record));
assertEquals(response.getInet4AddressRecord(), record);
}
@Test
public void getInet6AddressRecord_returnsAddedRecord() throws IOException {
DatagramPacket packet = new DatagramPacket(dataIn_ipv6_1, dataIn_ipv6_1.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
reader.skip(2); // skip record type indication.
MdnsInetAddressRecord record =
new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
MdnsResponse response = new MdnsResponse(0);
assertFalse(response.hasInet6AddressRecord());
assertTrue(response.setInet6AddressRecord(record));
assertEquals(response.getInet6AddressRecord(), record);
}
@Test
public void getPointerRecords_returnsAddedRecord() throws IOException {
DatagramPacket packet = new DatagramPacket(dataIn_ptr_1, dataIn_ptr_1.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
reader.skip(2); // skip record type indication.
MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
MdnsResponse response = new MdnsResponse(0);
assertFalse(response.hasPointerRecords());
assertTrue(response.addPointerRecord(record));
List<MdnsPointerRecord> recordList = response.getPointerRecords();
assertNotNull(recordList);
assertEquals(1, recordList.size());
assertEquals(record, recordList.get(0));
}
@Test
public void getServiceRecord_returnsAddedRecord() throws IOException {
DatagramPacket packet = new DatagramPacket(dataIn_service_1, dataIn_service_1.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
reader.skip(2); // skip record type indication.
MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
MdnsResponse response = new MdnsResponse(0);
assertFalse(response.hasServiceRecord());
assertTrue(response.setServiceRecord(record));
assertEquals(response.getServiceRecord(), record);
}
@Test
public void getTextRecord_returnsAddedRecord() throws IOException {
DatagramPacket packet = new DatagramPacket(dataIn_text_1, dataIn_text_1.length);
MdnsPacketReader reader = new MdnsPacketReader(packet);
String[] name = reader.readLabels();
reader.skip(2); // skip record type indication.
MdnsTextRecord record = new MdnsTextRecord(name, reader);
MdnsResponse response = new MdnsResponse(0);
assertFalse(response.hasTextRecord());
assertTrue(response.setTextRecord(record));
assertEquals(response.getTextRecord(), record);
}
@Test
public void mergeRecordsFrom_indicates_change_on_ipv4_address() throws IOException {
MdnsResponse response = makeMdnsResponse(
0,
Arrays.asList(
new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class)));
// Now create a new response that updates the address.
MdnsResponse response2 = makeMdnsResponse(
100,
Arrays.asList(
new PacketAndRecordClass(dataIn_ipv4_2, MdnsInet4AddressRecord.class)));
assertTrue(response.mergeRecordsFrom(response2));
}
@Test
public void mergeRecordsFrom_indicates_change_on_ipv6_address() throws IOException {
MdnsResponse response = makeMdnsResponse(
0,
Arrays.asList(
new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class)));
// Now create a new response that updates the address.
MdnsResponse response2 = makeMdnsResponse(
100,
Arrays.asList(
new PacketAndRecordClass(dataIn_ipv6_2, MdnsInet6AddressRecord.class)));
assertTrue(response.mergeRecordsFrom(response2));
}
@Test
public void mergeRecordsFrom_indicates_change_on_text() throws IOException {
MdnsResponse response = makeMdnsResponse(
0,
Arrays.asList(new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class)));
// Now create a new response that updates the address.
MdnsResponse response2 = makeMdnsResponse(
100,
Arrays.asList(new PacketAndRecordClass(dataIn_text_2, MdnsTextRecord.class)));
assertTrue(response.mergeRecordsFrom(response2));
}
@Test
public void mergeRecordsFrom_indicates_change_on_service() throws IOException {
MdnsResponse response = makeMdnsResponse(
0,
Arrays.asList(new PacketAndRecordClass(dataIn_service_1, MdnsServiceRecord.class)));
// Now create a new response that updates the address.
MdnsResponse response2 = makeMdnsResponse(
100,
Arrays.asList(new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class)));
assertTrue(response.mergeRecordsFrom(response2));
}
@Test
public void mergeRecordsFrom_indicates_change_on_pointer() throws IOException {
MdnsResponse response = makeMdnsResponse(
0,
Arrays.asList(new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class)));
// Now create a new response that updates the address.
MdnsResponse response2 = makeMdnsResponse(
100,
Arrays.asList(new PacketAndRecordClass(dataIn_ptr_2, MdnsPointerRecord.class)));
assertTrue(response.mergeRecordsFrom(response2));
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void mergeRecordsFrom_indicates_noChange() throws IOException {
//MdnsConfigsFlagsImpl.useReducedMergeRecordUpdateEvents.override(true);
List<PacketAndRecordClass> recordList =
Arrays.asList(
new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class),
new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class),
new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class),
new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class),
new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class));
// Create a two identical responses.
MdnsResponse response = makeMdnsResponse(0, recordList);
MdnsResponse response2 = makeMdnsResponse(100, recordList);
// Merging should not indicate any change.
assertFalse(response.mergeRecordsFrom(response2));
}
}

View File

@@ -0,0 +1,770 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/** Tests for {@link MdnsServiceTypeClient}. */
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsServiceTypeClientTests {
private static final String SERVICE_TYPE = "_googlecast._tcp.local";
@Mock
private MdnsServiceBrowserListener mockListenerOne;
@Mock
private MdnsServiceBrowserListener mockListenerTwo;
@Mock
private MdnsPacketWriter mockPacketWriter;
@Mock
private MdnsSocketClient mockSocketClient;
@Captor
private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
private final byte[] buf = new byte[10];
private DatagramPacket[] expectedPackets;
private ScheduledFuture<?>[] expectedSendFutures;
private FakeExecutor currentThreadExecutor = new FakeExecutor();
private MdnsServiceTypeClient client;
@Before
@SuppressWarnings("DoNotMock")
public void setUp() throws IOException {
MockitoAnnotations.initMocks(this);
expectedPackets = new DatagramPacket[16];
expectedSendFutures = new ScheduledFuture<?>[16];
for (int i = 0; i < expectedSendFutures.length; ++i) {
expectedPackets[i] = new DatagramPacket(buf, 0, 5);
expectedSendFutures[i] = Mockito.mock(ScheduledFuture.class);
}
when(mockPacketWriter.getPacket(any(SocketAddress.class)))
.thenReturn(expectedPackets[0])
.thenReturn(expectedPackets[1])
.thenReturn(expectedPackets[2])
.thenReturn(expectedPackets[3])
.thenReturn(expectedPackets[4])
.thenReturn(expectedPackets[5])
.thenReturn(expectedPackets[6])
.thenReturn(expectedPackets[7])
.thenReturn(expectedPackets[8])
.thenReturn(expectedPackets[9])
.thenReturn(expectedPackets[10])
.thenReturn(expectedPackets[11])
.thenReturn(expectedPackets[12])
.thenReturn(expectedPackets[13])
.thenReturn(expectedPackets[14])
.thenReturn(expectedPackets[15]);
client =
new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
@Override
MdnsPacketWriter createMdnsPacketWriter() {
return mockPacketWriter;
}
};
}
@Test
public void sendQueries_activeScanMode() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// First burst, 3 queries.
verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
verifyAndSendQuery(
1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Second burst will be sent after initialTimeBetweenBurstsMs, 3 queries.
verifyAndSendQuery(
3, MdnsConfigs.initialTimeBetweenBurstsMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
4, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
5, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Third burst will be sent after initialTimeBetweenBurstsMs * 2, 3 queries.
verifyAndSendQuery(
6, MdnsConfigs.initialTimeBetweenBurstsMs() * 2, /* expectsUnicastResponse= */
false);
verifyAndSendQuery(
7, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
8, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Forth burst will be sent after initialTimeBetweenBurstsMs * 4, 3 queries.
verifyAndSendQuery(
9, MdnsConfigs.initialTimeBetweenBurstsMs() * 4, /* expectsUnicastResponse= */
false);
verifyAndSendQuery(
10, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
11, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Fifth burst will be sent after timeBetweenBurstsMs, 3 queries.
verifyAndSendQuery(12, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
false);
verifyAndSendQuery(
13, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
14, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Stop sending packets.
client.stopSendAndReceive(mockListenerOne);
verify(expectedSendFutures[15]).cancel(true);
}
@Test
public void sendQueries_reentry_activeScanMode() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// First burst, first query is sent.
verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
// After the first query is sent, change the subtypes, and restart.
searchOptions =
MdnsSearchOptions.newBuilder()
.addSubtype("12345")
.addSubtype("abcde")
.setIsPassiveMode(false)
.build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// The previous scheduled task should be canceled.
verify(expectedSendFutures[1]).cancel(true);
// Queries should continue to be sent.
verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
verifyAndSendQuery(
2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Stop sending packets.
client.stopSendAndReceive(mockListenerOne);
verify(expectedSendFutures[5]).cancel(true);
}
@Test
public void sendQueries_passiveScanMode() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// First burst, 3 query.
verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
verifyAndSendQuery(
1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Second burst will be sent after timeBetweenBurstsMs, 1 query.
verifyAndSendQuery(3, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
false);
// Third burst will be sent after timeBetweenBurstsMs, 1 query.
verifyAndSendQuery(4, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
false);
// Stop sending packets.
client.stopSendAndReceive(mockListenerOne);
verify(expectedSendFutures[5]).cancel(true);
}
@Test
public void sendQueries_reentry_passiveScanMode() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// First burst, first query is sent.
verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
// After the first query is sent, change the subtypes, and restart.
searchOptions =
MdnsSearchOptions.newBuilder()
.addSubtype("12345")
.addSubtype("abcde")
.setIsPassiveMode(true)
.build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// The previous scheduled task should be canceled.
verify(expectedSendFutures[1]).cancel(true);
// Queries should continue to be sent.
verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
verifyAndSendQuery(
2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Stop sending packets.
client.stopSendAndReceive(mockListenerOne);
verify(expectedSendFutures[5]).cancel(true);
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
//MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
QueryTaskConfig config =
new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, 1);
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
// This is the first query of a new burst. We will ask for unicast response.
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertTrue(config.expectUnicastResponse);
assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@Test
public void testQueryTaskConfig_askForUnicastInFirstQuery() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
QueryTaskConfig config =
new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, 1);
// For the rest of queries in this burst, we will NOT ask for unicast response.
for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
// This is the first query of a new burst. We will NOT ask for unicast response.
int oldTransactionId = config.transactionId;
config = config.getConfigForNextRun();
assertFalse(config.expectUnicastResponse);
assertEquals(config.subtypes, searchOptions.getSubtypes());
assertEquals(config.transactionId, oldTransactionId + 1);
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
//MdnsConfigsFlagsImpl.useSessionIdToScheduleMdnsTask.override(true);
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
// Change the sutypes and start a new session.
searchOptions =
MdnsSearchOptions.newBuilder()
.addSubtype("12345")
.addSubtype("abcde")
.setIsPassiveMode(true)
.build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// Clear the scheduled runnable.
currentThreadExecutor.getAndClearLastScheduledRunnable();
// Simulate the case where the first mdns task is not successful canceled and it gets
// executed anyway.
firstMdnsTask.run();
// Although it gets executes, no more task gets scheduled.
assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void testIfPreviousTaskIsCanceledWhenSessionStops() {
//MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// Change the sutypes and start a new session.
client.stopSendAndReceive(mockListenerOne);
// Clear the scheduled runnable.
currentThreadExecutor.getAndClearLastScheduledRunnable();
// Simulate the case where the first mdns task is not successful canceled and it gets
// executed anyway.
currentThreadExecutor.getAndClearSubmittedRunnable().run();
// Although it gets executes, no more task gets scheduled.
assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
}
@Test
public void processResponse_incompleteResponse() {
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
MdnsResponse response = mock(MdnsResponse.class);
when(response.getServiceInstanceName()).thenReturn("service-instance-1");
when(response.isComplete()).thenReturn(false);
client.processResponse(response);
verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class));
verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class));
}
@Test
public void processIPv4Response_completeResponseForNewServiceInstance() throws Exception {
final String ipV4Address = "192.168.1.1";
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
"service-instance-1",
ipV4Address,
5353,
Collections.singletonList("ABCDE"),
Collections.emptyMap());
client.processResponse(initialResponse);
// Process a second response with a different port and updated text attributes.
MdnsResponse secondResponse =
createResponse(
"service-instance-1",
ipV4Address,
5354,
Collections.singletonList("ABCDE"),
Collections.singletonMap("key", "value"));
client.processResponse(secondResponse);
// Verify onServiceFound was called once for the initial response.
verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
assertEquals(initialServiceInfo.getIpv4Address(), ipV4Address);
assertEquals(initialServiceInfo.getPort(), 5353);
assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
assertNull(initialServiceInfo.getAttributeByKey("key"));
// Verify onServiceUpdated was called once for the second response.
verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
assertEquals(updatedServiceInfo.getIpv4Address(), ipV4Address);
assertEquals(updatedServiceInfo.getPort(), 5354);
assertTrue(updatedServiceInfo.hasSubtypes());
assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
}
@Test
public void processIPv6Response_getCorrectServiceInfo() throws Exception {
final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
"service-instance-1",
ipV6Address,
5353,
Collections.singletonList("ABCDE"),
Collections.emptyMap());
client.processResponse(initialResponse);
// Process a second response with a different port and updated text attributes.
MdnsResponse secondResponse =
createResponse(
"service-instance-1",
ipV6Address,
5354,
Collections.singletonList("ABCDE"),
Collections.singletonMap("key", "value"));
client.processResponse(secondResponse);
System.out.println("secondResponses ip"
+ secondResponse.getInet6AddressRecord().getInet6Address().getHostAddress());
// Verify onServiceFound was called once for the initial response.
verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
assertEquals(initialServiceInfo.getIpv6Address(), ipV6Address);
assertEquals(initialServiceInfo.getPort(), 5353);
assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
assertNull(initialServiceInfo.getAttributeByKey("key"));
// Verify onServiceUpdated was called once for the second response.
verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
assertEquals(updatedServiceInfo.getIpv6Address(), ipV6Address);
assertEquals(updatedServiceInfo.getPort(), 5354);
assertTrue(updatedServiceInfo.hasSubtypes());
assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
}
@Test
public void processResponse_goodBye() {
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
MdnsResponse response = mock(MdnsResponse.class);
when(response.getServiceInstanceName()).thenReturn("goodbye-service-instance-name");
when(response.isGoodbye()).thenReturn(true);
client.processResponse(response);
verify(mockListenerOne).onServiceRemoved("goodbye-service-instance-name");
verify(mockListenerTwo).onServiceRemoved("goodbye-service-instance-name");
}
@Test
public void reportExistingServiceToNewlyRegisteredListeners() throws UnknownHostException {
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
"service-instance-1",
"192.168.1.1",
5353,
Collections.singletonList("ABCDE"),
Collections.emptyMap());
client.processResponse(initialResponse);
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
// Verify onServiceFound was called once for the existing response.
verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(0);
assertEquals(existingServiceInfo.getServiceInstanceName(), "service-instance-1");
assertEquals(existingServiceInfo.getIpv4Address(), "192.168.1.1");
assertEquals(existingServiceInfo.getPort(), 5353);
assertEquals(existingServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
assertNull(existingServiceInfo.getAttributeByKey("key"));
// Process a goodbye message for the existing response.
MdnsResponse goodByeResponse = mock(MdnsResponse.class);
when(goodByeResponse.getServiceInstanceName()).thenReturn("service-instance-1");
when(goodByeResponse.isGoodbye()).thenReturn(true);
client.processResponse(goodByeResponse);
client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
// Verify onServiceFound was not called on the newly registered listener after the existing
// response is gone.
verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class));
}
@Test
public void processResponse_notAllowRemoveSearch_shouldNotRemove() throws Exception {
final String serviceInstanceName = "service-instance-1";
client.startSendAndReceive(
mockListenerOne,
MdnsSearchOptions.newBuilder().build());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
Map.of());
client.processResponse(initialResponse);
// Clear the scheduled runnable.
currentThreadExecutor.getAndClearLastScheduledRunnable();
// Simulate the case where the response is after TTL.
when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
firstMdnsTask.run();
// Verify onServiceRemoved was not called.
verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void processResponse_allowSearchOptionsToRemoveExpiredService_shouldRemove()
throws Exception {
//MdnsConfigsFlagsImpl.allowSearchOptionsToRemoveExpiredService.override(true);
final String serviceInstanceName = "service-instance-1";
client =
new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
@Override
MdnsPacketWriter createMdnsPacketWriter() {
return mockPacketWriter;
}
};
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
Map.of());
client.processResponse(initialResponse);
// Clear the scheduled runnable.
currentThreadExecutor.getAndClearLastScheduledRunnable();
// Simulate the case where the response is under TTL.
when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 1000);
firstMdnsTask.run();
// Verify onServiceRemoved was not called.
verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
// Simulate the case where the response is after TTL.
when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
firstMdnsTask.run();
// Verify onServiceRemoved was called.
verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
}
@Test
public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
throws Exception {
final String serviceInstanceName = "service-instance-1";
client =
new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
@Override
MdnsPacketWriter createMdnsPacketWriter() {
return mockPacketWriter;
}
};
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
Map.of());
client.processResponse(initialResponse);
// Clear the scheduled runnable.
currentThreadExecutor.getAndClearLastScheduledRunnable();
// Simulate the case where the response is after TTL.
when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
firstMdnsTask.run();
// Verify onServiceRemoved was not called.
verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void processResponse_removeServiceAfterTtlExpiresEnabled_shouldRemove()
throws Exception {
//MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
final String serviceInstanceName = "service-instance-1";
client =
new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
@Override
MdnsPacketWriter createMdnsPacketWriter() {
return mockPacketWriter;
}
};
client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
Map.of());
client.processResponse(initialResponse);
// Clear the scheduled runnable.
currentThreadExecutor.getAndClearLastScheduledRunnable();
// Simulate the case where the response is after TTL.
when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
firstMdnsTask.run();
// Verify onServiceRemoved was not called.
verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
}
// verifies that the right query was enqueued with the right delay, and send query by executing
// the runnable.
private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse) {
assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
if (expectsUnicastResponse) {
verify(mockSocketClient).sendUnicastPacket(expectedPackets[index]);
} else {
verify(mockSocketClient).sendMulticastPacket(expectedPackets[index]);
}
}
// A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
// time.
private class FakeExecutor extends ScheduledThreadPoolExecutor {
private long lastScheduledDelayInMs;
private Runnable lastScheduledRunnable;
private Runnable lastSubmittedRunnable;
private int futureIndex;
FakeExecutor() {
super(1);
lastScheduledDelayInMs = -1;
}
@Override
public Future<?> submit(Runnable command) {
Future<?> future = super.submit(command);
lastSubmittedRunnable = command;
return future;
}
// Don't call through the real implementation, just track the scheduled Runnable, and
// returns a ScheduledFuture.
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
lastScheduledDelayInMs = delay;
lastScheduledRunnable = command;
return expectedSendFutures[futureIndex++];
}
// Returns the delay of the last scheduled task, and clear it.
long getAndClearLastScheduledDelayInMs() {
long val = lastScheduledDelayInMs;
lastScheduledDelayInMs = -1;
return val;
}
// Returns the last scheduled task, and clear it.
Runnable getAndClearLastScheduledRunnable() {
Runnable val = lastScheduledRunnable;
lastScheduledRunnable = null;
return val;
}
Runnable getAndClearSubmittedRunnable() {
Runnable val = lastSubmittedRunnable;
lastSubmittedRunnable = null;
return val;
}
}
// Creates a complete mDNS response.
private MdnsResponse createResponse(
@NonNull String serviceInstanceName,
@NonNull String host,
int port,
@NonNull List<String> subtypes,
@NonNull Map<String, String> textAttributes)
throws UnknownHostException {
String[] hostName = new String[]{"hostname"};
MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class);
when(serviceRecord.getServiceHost()).thenReturn(hostName);
when(serviceRecord.getServicePort()).thenReturn(port);
MdnsResponse response = spy(new MdnsResponse(0));
MdnsInetAddressRecord inetAddressRecord = mock(MdnsInetAddressRecord.class);
if (host.contains(":")) {
when(inetAddressRecord.getInet6Address())
.thenReturn((Inet6Address) Inet6Address.getByName(host));
response.setInet6AddressRecord(inetAddressRecord);
} else {
when(inetAddressRecord.getInet4Address())
.thenReturn((Inet4Address) Inet4Address.getByName(host));
response.setInet4AddressRecord(inetAddressRecord);
}
MdnsTextRecord textRecord = mock(MdnsTextRecord.class);
List<String> textStrings = new ArrayList<>();
for (Map.Entry<String, String> kv : textAttributes.entrySet()) {
textStrings.add(kv.getKey() + "=" + kv.getValue());
}
when(textRecord.getStrings()).thenReturn(textStrings);
response.setServiceRecord(serviceRecord);
response.setTextRecord(textRecord);
doReturn(false).when(response).isGoodbye();
doReturn(true).when(response).isComplete();
doReturn(serviceInstanceName).when(response).getServiceInstanceName();
doReturn(new ArrayList<>(subtypes)).when(response).getSubtypes();
return response;
}
}

View File

@@ -0,0 +1,493 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.Manifest.permission;
import android.annotation.RequiresPermission;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.text.format.DateUtils;
import com.android.net.module.util.HexDump;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import java.io.IOException;
import java.net.DatagramPacket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/** Tests for {@link MdnsSocketClient} */
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsSocketClientTests {
private static final long TIMEOUT = 500;
private final byte[] buf = new byte[10];
final AtomicBoolean enableMulticastResponse = new AtomicBoolean(true);
final AtomicBoolean enableUnicastResponse = new AtomicBoolean(true);
@Mock private Context mContext;
@Mock private WifiManager mockWifiManager;
@Mock private MdnsSocket mockMulticastSocket;
@Mock private MdnsSocket mockUnicastSocket;
@Mock private MulticastLock mockMulticastLock;
@Mock private MdnsSocketClient.Callback mockCallback;
private MdnsSocketClient mdnsClient;
@Before
public void setup() throws RuntimeException, IOException {
MockitoAnnotations.initMocks(this);
when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
.thenReturn(mockMulticastLock);
mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
@Override
MdnsSocket createMdnsSocket(int port) throws IOException {
if (port == MdnsConstants.MDNS_PORT) {
return mockMulticastSocket;
}
return mockUnicastSocket;
}
};
mdnsClient.setCallback(mockCallback);
doAnswer(
(InvocationOnMock invocationOnMock) -> {
final byte[] dataIn = HexDump.hexStringToByteArray(
"0000840000000004"
+ "00000003134A6F68"
+ "6E6E792773204368"
+ "726F6D6563617374"
+ "0B5F676F6F676C65"
+ "63617374045F7463"
+ "70056C6F63616C00"
+ "0010800100001194"
+ "006C2369643D3937"
+ "3062663534376237"
+ "3533666336336332"
+ "6432613336626238"
+ "3936616261380576"
+ "653D30320D6D643D"
+ "4368726F6D656361"
+ "73741269633D2F73"
+ "657475702F69636F"
+ "6E2E706E6716666E"
+ "3D4A6F686E6E7927"
+ "73204368726F6D65"
+ "636173740463613D"
+ "350473743D30095F"
+ "7365727669636573"
+ "075F646E732D7364"
+ "045F756470C03100"
+ "0C00010000119400"
+ "02C020C020000C00"
+ "01000011940002C0"
+ "0CC00C0021800100"
+ "000078001C000000"
+ "001F49134A6F686E"
+ "6E79277320436872"
+ "6F6D6563617374C0"
+ "31C0F30001800100"
+ "0000780004C0A864"
+ "68C0F3002F800100"
+ "0000780005C0F300"
+ "0140C00C002F8001"
+ "000011940009C00C"
+ "00050000800040");
if (enableMulticastResponse.get()) {
DatagramPacket packet = invocationOnMock.getArgument(0);
packet.setData(dataIn);
}
return null;
})
.when(mockMulticastSocket)
.receive(any(DatagramPacket.class));
doAnswer(
(InvocationOnMock invocationOnMock) -> {
final byte[] dataIn = HexDump.hexStringToByteArray(
"0000840000000004"
+ "00000003134A6F68"
+ "6E6E792773204368"
+ "726F6D6563617374"
+ "0B5F676F6F676C65"
+ "63617374045F7463"
+ "70056C6F63616C00"
+ "0010800100001194"
+ "006C2369643D3937"
+ "3062663534376237"
+ "3533666336336332"
+ "6432613336626238"
+ "3936616261380576"
+ "653D30320D6D643D"
+ "4368726F6D656361"
+ "73741269633D2F73"
+ "657475702F69636F"
+ "6E2E706E6716666E"
+ "3D4A6F686E6E7927"
+ "73204368726F6D65"
+ "636173740463613D"
+ "350473743D30095F"
+ "7365727669636573"
+ "075F646E732D7364"
+ "045F756470C03100"
+ "0C00010000119400"
+ "02C020C020000C00"
+ "01000011940002C0"
+ "0CC00C0021800100"
+ "000078001C000000"
+ "001F49134A6F686E"
+ "6E79277320436872"
+ "6F6D6563617374C0"
+ "31C0F30001800100"
+ "0000780004C0A864"
+ "68C0F3002F800100"
+ "0000780005C0F300"
+ "0140C00C002F8001"
+ "000011940009C00C"
+ "00050000800040");
if (enableUnicastResponse.get()) {
DatagramPacket packet = invocationOnMock.getArgument(0);
packet.setData(dataIn);
}
return null;
})
.when(mockUnicastSocket)
.receive(any(DatagramPacket.class));
}
@After
public void tearDown() {
mdnsClient.stopDiscovery();
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void testSendPackets_useSeparateSocketForUnicast()
throws InterruptedException, IOException {
//MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
//MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
//MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
// .override(DateUtils.SECOND_IN_MILLIS);
mdnsClient.startDiscovery();
Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
Thread sendThread = mdnsClient.sendThread;
assertTrue(multicastReceiverThread.isAlive());
assertTrue(sendThread.isAlive());
assertTrue(unicastReceiverThread.isAlive());
// Sends a packet.
DatagramPacket packet = new DatagramPacket(buf, 0, 5);
mdnsClient.sendMulticastPacket(packet);
// mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
// it may not be called yet. So timeout is added.
verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
// Verify the packet is sent by the unicast socket.
mdnsClient.sendUnicastPacket(packet);
verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
// Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
// Run part of the test logic in a background thread, in case stopDiscovery() blocks
// for a long time (the foreground thread can fail the test early).
final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
Thread testThread =
new Thread(
new Runnable() {
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
@Override
public void run() {
mdnsClient.stopDiscovery();
stopDiscoveryLatch.countDown();
}
});
testThread.start();
assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
// We should be able to join in a reasonable amount of time, to prove that the
// the MdnsClient exited without sending the large queue of packets.
testThread.join(DateUtils.SECOND_IN_MILLIS);
assertFalse(multicastReceiverThread.isAlive());
assertFalse(sendThread.isAlive());
assertFalse(unicastReceiverThread.isAlive());
}
@Test
public void testSendPackets_useSameSocketForMulticastAndUnicast()
throws InterruptedException, IOException {
mdnsClient.startDiscovery();
Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
Thread sendThread = mdnsClient.sendThread;
assertTrue(multicastReceiverThread.isAlive());
assertTrue(sendThread.isAlive());
assertNull(unicastReceiverThread);
// Sends a packet.
DatagramPacket packet = new DatagramPacket(buf, 0, 5);
mdnsClient.sendMulticastPacket(packet);
// mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
// it may not be called yet. So timeout is added.
verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
// Verify the packet is sent by the multicast socket as well.
mdnsClient.sendUnicastPacket(packet);
verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
// Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
// Run part of the test logic in a background thread, in case stopDiscovery() blocks
// for a long time (the foreground thread can fail the test early).
final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
Thread testThread =
new Thread(
new Runnable() {
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
@Override
public void run() {
mdnsClient.stopDiscovery();
stopDiscoveryLatch.countDown();
}
});
testThread.start();
assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
// We should be able to join in a reasonable amount of time, to prove that the
// the MdnsClient exited without sending the large queue of packets.
testThread.join(DateUtils.SECOND_IN_MILLIS);
assertFalse(multicastReceiverThread.isAlive());
assertFalse(sendThread.isAlive());
assertNull(unicastReceiverThread);
}
@Test
public void testStartStop() throws IOException {
for (int i = 0; i < 5; i++) {
mdnsClient.startDiscovery();
Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
Thread socketThread = mdnsClient.sendThread;
assertTrue(multicastReceiverThread.isAlive());
assertTrue(socketThread.isAlive());
mdnsClient.stopDiscovery();
assertFalse(multicastReceiverThread.isAlive());
assertFalse(socketThread.isAlive());
}
}
@Test
public void testStopDiscovery_queueIsCleared() throws IOException {
mdnsClient.startDiscovery();
mdnsClient.stopDiscovery();
mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
synchronized (mdnsClient.multicastPacketQueue) {
assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
}
}
@Test
public void testSendPacket_afterDiscoveryStops() throws IOException {
mdnsClient.startDiscovery();
mdnsClient.stopDiscovery();
mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
synchronized (mdnsClient.multicastPacketQueue) {
assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
}
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void testSendPacket_queueReachesSizeLimit() throws IOException {
//MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
mdnsClient.startDiscovery();
for (int i = 0; i < 100; i++) {
mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
}
synchronized (mdnsClient.multicastPacketQueue) {
assertTrue(mdnsClient.multicastPacketQueue.size() <= 2);
}
}
@Test
public void testMulticastResponseReceived_useSeparateSocketForUnicast() throws IOException {
mdnsClient.setCallback(mockCallback);
mdnsClient.startDiscovery();
verify(mockCallback, timeout(TIMEOUT).atLeast(1))
.onResponseReceived(any(MdnsResponse.class));
}
@Test
public void testMulticastResponseReceived_useSameSocketForMulticastAndUnicast()
throws Exception {
mdnsClient.startDiscovery();
verify(mockCallback, timeout(TIMEOUT).atLeastOnce())
.onResponseReceived(any(MdnsResponse.class));
mdnsClient.stopDiscovery();
}
@Test
public void testFailedToParseMdnsResponse_useSeparateSocketForUnicast() throws IOException {
mdnsClient.setCallback(mockCallback);
// Both multicast socket and unicast socket receive malformed responses.
byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
doAnswer(
(InvocationOnMock invocationOnMock) -> {
// Malformed data.
DatagramPacket packet = invocationOnMock.getArgument(0);
packet.setData(dataIn);
return null;
})
.when(mockMulticastSocket)
.receive(any(DatagramPacket.class));
doAnswer(
(InvocationOnMock invocationOnMock) -> {
// Malformed data.
DatagramPacket packet = invocationOnMock.getArgument(0);
packet.setData(dataIn);
return null;
})
.when(mockUnicastSocket)
.receive(any(DatagramPacket.class));
mdnsClient.startDiscovery();
verify(mockCallback, timeout(TIMEOUT).atLeast(1))
.onFailedToParseMdnsResponse(anyInt(), eq(MdnsResponseErrorCode.ERROR_END_OF_FILE));
mdnsClient.stopDiscovery();
}
@Test
public void testFailedToParseMdnsResponse_useSameSocketForMulticastAndUnicast()
throws IOException {
doAnswer(
(InvocationOnMock invocationOnMock) -> {
final byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
DatagramPacket packet = invocationOnMock.getArgument(0);
packet.setData(dataIn);
return null;
})
.when(mockMulticastSocket)
.receive(any(DatagramPacket.class));
mdnsClient.startDiscovery();
verify(mockCallback, timeout(TIMEOUT).atLeast(1))
.onFailedToParseMdnsResponse(1, MdnsResponseErrorCode.ERROR_END_OF_FILE);
mdnsClient.stopDiscovery();
}
@Test
@Ignore("MdnsConfigs is not configurable currently.")
public void testMulticastResponseIsNotReceived() throws IOException, InterruptedException {
//MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
//MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
// .override(DateUtils.SECOND_IN_MILLIS);
//MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
enableMulticastResponse.set(false);
enableUnicastResponse.set(true);
mdnsClient.startDiscovery();
DatagramPacket packet = new DatagramPacket(buf, 0, 5);
mdnsClient.sendUnicastPacket(packet);
mdnsClient.sendMulticastPacket(packet);
// Wait for the timer to be triggered.
Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
assertFalse(mdnsClient.receivedMulticastResponse);
assertTrue(mdnsClient.receivedUnicastResponse);
assertTrue(mdnsClient.cannotReceiveMulticastResponse.get());
// Allow multicast response and verify the states again.
enableMulticastResponse.set(true);
Thread.sleep(DateUtils.SECOND_IN_MILLIS);
// Verify cannotReceiveMulticastResponse is reset to false.
assertTrue(mdnsClient.receivedMulticastResponse);
assertTrue(mdnsClient.receivedUnicastResponse);
assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
// Stop the discovery and start a new session. Don't respond the unicsat query either in
// this session.
enableMulticastResponse.set(false);
enableUnicastResponse.set(false);
mdnsClient.stopDiscovery();
mdnsClient.startDiscovery();
// Verify the states are reset.
assertFalse(mdnsClient.receivedMulticastResponse);
assertFalse(mdnsClient.receivedUnicastResponse);
assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
mdnsClient.sendUnicastPacket(packet);
mdnsClient.sendMulticastPacket(packet);
Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
// Verify cannotReceiveMulticastResponse is not set the true because we didn't receive the
// unicast response either. This is expected for users who don't have any cast device.
assertFalse(mdnsClient.receivedMulticastResponse);
assertFalse(mdnsClient.receivedUnicastResponse);
assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collections;
/** Tests for {@link MdnsSocket}. */
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsSocketTests {
@Mock private NetworkInterfaceWrapper mockNetworkInterfaceWrapper;
@Mock private MulticastSocket mockMulticastSocket;
@Mock private MulticastNetworkInterfaceProvider mockMulticastNetworkInterfaceProvider;
private SocketAddress socketIPv4Address;
private SocketAddress socketIPv6Address;
private byte[] data = new byte[25];
private final DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
private NetworkInterface networkInterface;
private MdnsSocket mdnsSocket;
@Before
public void setUp() throws SocketException, UnknownHostException {
MockitoAnnotations.initMocks(this);
networkInterface = createEmptyNetworkInterface();
when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
.thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
socketIPv4Address = new InetSocketAddress(
InetAddress.getByName("224.0.0.251"), MdnsConstants.MDNS_PORT);
socketIPv6Address = new InetSocketAddress(
InetAddress.getByName("FF02::FB"), MdnsConstants.MDNS_PORT);
}
@Test
public void testMdnsSocket() throws IOException {
mdnsSocket =
new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
@Override
MulticastSocket createMulticastSocket(int port) throws IOException {
return mockMulticastSocket;
}
};
mdnsSocket.send(datagramPacket);
verify(mockMulticastSocket).setNetworkInterface(networkInterface);
verify(mockMulticastSocket).send(datagramPacket);
mdnsSocket.receive(datagramPacket);
verify(mockMulticastSocket).receive(datagramPacket);
mdnsSocket.joinGroup();
verify(mockMulticastSocket).joinGroup(socketIPv4Address, networkInterface);
mdnsSocket.leaveGroup();
verify(mockMulticastSocket).leaveGroup(socketIPv4Address, networkInterface);
mdnsSocket.close();
verify(mockMulticastSocket).close();
}
@Test
public void testIPv6OnlyNetwork_IPv6Enabled() throws IOException {
// Have mockMulticastNetworkInterfaceProvider send back an IPv6Only networkInterfaceWrapper
networkInterface = createEmptyNetworkInterface();
when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
.thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
mdnsSocket =
new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
@Override
MulticastSocket createMulticastSocket(int port) throws IOException {
return mockMulticastSocket;
}
};
when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
Collections.singletonList(mockNetworkInterfaceWrapper)))
.thenReturn(true);
mdnsSocket.joinGroup();
verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
mdnsSocket.leaveGroup();
verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
mdnsSocket.close();
verify(mockMulticastSocket).close();
}
@Test
public void testIPv6OnlyNetwork_IPv6Toggle() throws IOException {
// Have mockMulticastNetworkInterfaceProvider send back a networkInterfaceWrapper
networkInterface = createEmptyNetworkInterface();
when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
.thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
mdnsSocket =
new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
@Override
MulticastSocket createMulticastSocket(int port) throws IOException {
return mockMulticastSocket;
}
};
when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
Collections.singletonList(mockNetworkInterfaceWrapper)))
.thenReturn(true);
mdnsSocket.joinGroup();
verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
mdnsSocket.leaveGroup();
verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
mdnsSocket.close();
verify(mockMulticastSocket).close();
}
private NetworkInterface createEmptyNetworkInterface() {
try {
Constructor<NetworkInterface> constructor =
NetworkInterface.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,275 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.server.connectivity.mdns;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Tests for {@link MulticastNetworkInterfaceProvider}. */
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MulticastNetworkInterfaceProviderTests {
@Mock private NetworkInterfaceWrapper loopbackInterface;
@Mock private NetworkInterfaceWrapper pointToPointInterface;
@Mock private NetworkInterfaceWrapper virtualInterface;
@Mock private NetworkInterfaceWrapper inactiveMulticastInterface;
@Mock private NetworkInterfaceWrapper activeIpv6MulticastInterface;
@Mock private NetworkInterfaceWrapper activeIpv6MulticastInterfaceTwo;
@Mock private NetworkInterfaceWrapper nonMulticastInterface;
@Mock private NetworkInterfaceWrapper multicastInterfaceOne;
@Mock private NetworkInterfaceWrapper multicastInterfaceTwo;
private final List<NetworkInterfaceWrapper> networkInterfaces = new ArrayList<>();
private MulticastNetworkInterfaceProvider provider;
private Context context;
@Before
public void setUp() throws SocketException, UnknownHostException {
MockitoAnnotations.initMocks(this);
context = InstrumentationRegistry.getContext();
setupNetworkInterface(
loopbackInterface,
true /* isUp */,
true /* isLoopBack */,
false /* isPointToPoint */,
false /* isVirtual */,
true /* supportsMulticast */,
false /* isIpv6 */);
setupNetworkInterface(
pointToPointInterface,
true /* isUp */,
false /* isLoopBack */,
true /* isPointToPoint */,
false /* isVirtual */,
true /* supportsMulticast */,
false /* isIpv6 */);
setupNetworkInterface(
virtualInterface,
true /* isUp */,
false /* isLoopBack */,
false /* isPointToPoint */,
true /* isVirtual */,
true /* supportsMulticast */,
false /* isIpv6 */);
setupNetworkInterface(
inactiveMulticastInterface,
false /* isUp */,
false /* isLoopBack */,
false /* isPointToPoint */,
false /* isVirtual */,
true /* supportsMulticast */,
false /* isIpv6 */);
setupNetworkInterface(
nonMulticastInterface,
true /* isUp */,
false /* isLoopBack */,
false /* isPointToPoint */,
false /* isVirtual */,
false /* supportsMulticast */,
false /* isIpv6 */);
setupNetworkInterface(
activeIpv6MulticastInterface,
true /* isUp */,
false /* isLoopBack */,
false /* isPointToPoint */,
false /* isVirtual */,
true /* supportsMulticast */,
true /* isIpv6 */);
setupNetworkInterface(
activeIpv6MulticastInterfaceTwo,
true /* isUp */,
false /* isLoopBack */,
false /* isPointToPoint */,
false /* isVirtual */,
true /* supportsMulticast */,
true /* isIpv6 */);
setupNetworkInterface(
multicastInterfaceOne,
true /* isUp */,
false /* isLoopBack */,
false /* isPointToPoint */,
false /* isVirtual */,
true /* supportsMulticast */,
false /* isIpv6 */);
setupNetworkInterface(
multicastInterfaceTwo,
true /* isUp */,
false /* isLoopBack */,
false /* isPointToPoint */,
false /* isVirtual */,
true /* supportsMulticast */,
false /* isIpv6 */);
provider =
new MulticastNetworkInterfaceProvider(context) {
@Override
List<NetworkInterfaceWrapper> getNetworkInterfaces() {
return networkInterfaces;
}
};
}
@Test
public void testGetMulticastNetworkInterfaces() {
// getNetworkInterfaces returns 1 multicast interface and 5 interfaces that can not be used
// to send and receive multicast packets.
networkInterfaces.add(loopbackInterface);
networkInterfaces.add(pointToPointInterface);
networkInterfaces.add(virtualInterface);
networkInterfaces.add(inactiveMulticastInterface);
networkInterfaces.add(nonMulticastInterface);
networkInterfaces.add(multicastInterfaceOne);
assertEquals(Collections.singletonList(multicastInterfaceOne),
provider.getMulticastNetworkInterfaces());
// getNetworkInterfaces returns 2 multicast interfaces after a connectivity change.
networkInterfaces.clear();
networkInterfaces.add(multicastInterfaceOne);
networkInterfaces.add(multicastInterfaceTwo);
provider.connectivityMonitor.notifyConnectivityChange();
assertEquals(networkInterfaces, provider.getMulticastNetworkInterfaces());
}
@Test
public void testStartWatchingConnectivityChanges() {
ConnectivityMonitor mockMonitor = mock(ConnectivityMonitor.class);
provider.connectivityMonitor = mockMonitor;
InOrder inOrder = inOrder(mockMonitor);
provider.startWatchingConnectivityChanges();
inOrder.verify(mockMonitor).startWatchingConnectivityChanges();
provider.stopWatchingConnectivityChanges();
inOrder.verify(mockMonitor).stopWatchingConnectivityChanges();
}
@Test
public void testIpV6OnlyNetwork_EmptyNetwork() {
// getNetworkInterfaces returns no network interfaces.
assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
}
@Test
public void testIpV6OnlyNetwork_IPv4Only() {
// getNetworkInterfaces returns two IPv4 network interface.
networkInterfaces.add(multicastInterfaceOne);
networkInterfaces.add(multicastInterfaceTwo);
assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
}
@Test
public void testIpV6OnlyNetwork_MixedNetwork() {
// getNetworkInterfaces returns one IPv6 network interface.
networkInterfaces.add(activeIpv6MulticastInterface);
networkInterfaces.add(multicastInterfaceOne);
networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
networkInterfaces.add(multicastInterfaceTwo);
assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
}
@Test
public void testIpV6OnlyNetwork_IPv6Only() {
// getNetworkInterfaces returns one IPv6 network interface.
networkInterfaces.add(activeIpv6MulticastInterface);
networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
}
@Test
public void testIpV6OnlyNetwork_IPv6Enabled() {
// getNetworkInterfaces returns one IPv6 network interface.
networkInterfaces.add(activeIpv6MulticastInterface);
assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
final List<NetworkInterfaceWrapper> interfaces = provider.getMulticastNetworkInterfaces();
assertEquals(Collections.singletonList(activeIpv6MulticastInterface), interfaces);
}
private void setupNetworkInterface(
@NonNull NetworkInterfaceWrapper networkInterfaceWrapper,
boolean isUp,
boolean isLoopback,
boolean isPointToPoint,
boolean isVirtual,
boolean supportsMulticast,
boolean isIpv6)
throws SocketException, UnknownHostException {
when(networkInterfaceWrapper.isUp()).thenReturn(isUp);
when(networkInterfaceWrapper.isLoopback()).thenReturn(isLoopback);
when(networkInterfaceWrapper.isPointToPoint()).thenReturn(isPointToPoint);
when(networkInterfaceWrapper.isVirtual()).thenReturn(isVirtual);
when(networkInterfaceWrapper.supportsMulticast()).thenReturn(supportsMulticast);
if (isIpv6) {
InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
InetAddress ip6Address = Inet6Address.getByName("2001:4860:0:1001::68");
when(interfaceAddress.getAddress()).thenReturn(ip6Address);
when(networkInterfaceWrapper.getInterfaceAddresses())
.thenReturn(Collections.singletonList(interfaceAddress));
} else {
Inet4Address ip = (Inet4Address) Inet4Address.getByName("192.168.0.1");
InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
when(interfaceAddress.getAddress()).thenReturn(ip);
when(networkInterfaceWrapper.getInterfaceAddresses())
.thenReturn(Collections.singletonList(interfaceAddress));
}
}
}