mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-09-05 16:45:08 +00:00
`DeviceInfo` contains all the properties we need to instantiate a `Device`: id, name, type, cert, capabilities and protocol version. Before, we had a mix of passing those around as arguments or passing identity packets (ie: json). This simplifies the `Device` class quite a bit. Now, `BaseLink` subclasses need to implement the `getDeviceInfo()` interface that returns a `DeviceInfo`, which is what we will pass around and eventually use to instantiate a `Device`. This means that identity packets are an implementation detail of the `LanLinkProvider` and that other implementations could get the `DeviceInfo` in a different way. In a future, we can add a mechanism for links to notify when their `DeviceInfo` changed. This will allow us to better support device renames (which now are implemented by reconnecting to the device and don't always work) or sending the capabilities fields later in the case of MacOS/iOS where we can't send them in a UDP packet due to the size limit of 1500 bytes.
209 lines
11 KiB
Java
209 lines
11 KiB
Java
/*
|
|
* SPDX-FileCopyrightText: 2015 Vineet Garg <grg.vineet@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
*/
|
|
|
|
package org.kde.kdeconnect;
|
|
|
|
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.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyInt;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
|
|
import android.app.NotificationManager;
|
|
import android.content.Context;
|
|
import android.content.SharedPreferences;
|
|
import android.preference.PreferenceManager;
|
|
import android.util.Base64;
|
|
import android.util.Log;
|
|
|
|
import androidx.core.content.ContextCompat;
|
|
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.kde.kdeconnect.Backends.LanBackend.LanLink;
|
|
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
|
|
import org.kde.kdeconnect.Helpers.DeviceHelper;
|
|
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
|
|
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
|
import org.mockito.Mockito;
|
|
import org.powermock.api.mockito.PowerMockito;
|
|
import org.powermock.core.classloader.annotations.PrepareForTest;
|
|
import org.powermock.modules.junit4.PowerMockRunner;
|
|
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.security.cert.Certificate;
|
|
import java.security.cert.CertificateException;
|
|
|
|
@RunWith(PowerMockRunner.class)
|
|
@PrepareForTest({Base64.class, Log.class, PreferenceManager.class, ContextCompat.class})
|
|
public class DeviceTest {
|
|
|
|
private Context context;
|
|
|
|
// Creating a paired device before each test case
|
|
@Before
|
|
public void setUp() {
|
|
// Save new test device in settings
|
|
|
|
String deviceId = "testDevice";
|
|
String name = "Test Device";
|
|
String encodedCertificate = "MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
|
|
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
|
|
"bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5\n" +
|
|
"MV9mMDY0XzQ3OGVfYmU4Y18xOTM1ZDc1NDRkNTRfMQwwCgYDVQQKDANLREUxFDASBgNVBAsMC0tk\n" +
|
|
"ZSBjb25uZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzH9GxS1lctpwYdSGAoPH\n" +
|
|
"ws+MnVaL0PVDCuzrpxzXc+bChR87xofhQIesLPLZEcmUJ1MlEJ6jx4W+gVhvY2tUN7SoiKKbnq8s\n" +
|
|
"WjI5ovs5yML3C1zPbOSJAdK613FcdkK+UGd/9dQk54gIozinC58iyTAChVVpB3pAF38EPxwKkuo2\n" +
|
|
"qTzwk24d6PRxz1skkzwEphUQQzGboyHsAlJHN1MzM2/yFGB4l8iUua2d3ETyfy/xFEh/SwtGtXE5\n" +
|
|
"KLz4cpb0fxjeYQZVruBKxzE07kgDO3zOhmP3LJ/KSPHWYImd1DWmpY9iDvoXr6+V7FAnRloaEIyg\n" +
|
|
"7WwdlSCpo3TXVuIjLwIDAQABozIwMDAdBgNVHQ4EFgQUwmbHo8YbiR463GRKSLL3eIKyvDkwDwYD\n" +
|
|
"VR0TAQH/BAUwAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAydijH3rbnvpBDB/30w2PCGMT7O0N/XYM\n" +
|
|
"wBtUidqa4NFumJrNrccx5Ehp4UP66BfP61HW8h2U/EekYfOsZyyWd4KnsDD6ycR8h/WvpK3BC2cn\n" +
|
|
"I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r\n" +
|
|
"/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl\n" +
|
|
"yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg\n" +
|
|
"7n+KOQ==";
|
|
|
|
this.context = Mockito.mock(Context.class);
|
|
|
|
PowerMockito.mockStatic(Base64.class);
|
|
PowerMockito.when(Base64.encodeToString(any(), anyInt())).thenAnswer(invocation -> java.util.Base64.getMimeEncoder().encodeToString((byte[]) invocation.getArguments()[0]));
|
|
PowerMockito.when(Base64.decode(anyString(), anyInt())).thenAnswer(invocation -> java.util.Base64.getMimeDecoder().decode((String) invocation.getArguments()[0]));
|
|
|
|
PowerMockito.mockStatic(Log.class);
|
|
|
|
//Store device information needed to create a Device object in a future
|
|
MockSharedPreference deviceSettings = new MockSharedPreference();
|
|
SharedPreferences.Editor editor = deviceSettings.edit();
|
|
editor.putString("deviceName", name);
|
|
editor.putString("deviceType", DeviceType.Phone.toString());
|
|
editor.putString("certificate", encodedCertificate);
|
|
editor.apply();
|
|
Mockito.when(context.getSharedPreferences(eq(deviceId), eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings);
|
|
|
|
//Store the device as trusted
|
|
MockSharedPreference trustedSettings = new MockSharedPreference();
|
|
trustedSettings.edit().putBoolean(deviceId, true).apply();
|
|
Mockito.when(context.getSharedPreferences(eq("trusted_devices"), eq(Context.MODE_PRIVATE))).thenReturn(trustedSettings);
|
|
|
|
//Store an untrusted device
|
|
MockSharedPreference untrustedSettings = new MockSharedPreference();
|
|
Mockito.when(context.getSharedPreferences(eq("unpairedTestDevice"), eq(Context.MODE_PRIVATE))).thenReturn(untrustedSettings);
|
|
|
|
//Default shared prefs, including our own private key
|
|
PowerMockito.mockStatic(PreferenceManager.class);
|
|
MockSharedPreference defaultSettings = new MockSharedPreference();
|
|
PowerMockito.when(PreferenceManager.getDefaultSharedPreferences(any())).thenReturn(defaultSettings);
|
|
RsaHelper.initialiseRsaKeys(context);
|
|
|
|
PowerMockito.mockStatic(ContextCompat.class);
|
|
PowerMockito.when(ContextCompat.getSystemService(context, NotificationManager.class)).thenReturn(Mockito.mock(NotificationManager.class));
|
|
}
|
|
|
|
@Test
|
|
public void testDeviceType() {
|
|
assertEquals(DeviceType.Phone, DeviceType.fromString(DeviceType.Phone.toString()));
|
|
assertEquals(DeviceType.Tablet, DeviceType.fromString(DeviceType.Tablet.toString()));
|
|
assertEquals(DeviceType.Computer, DeviceType.fromString(DeviceType.Computer.toString()));
|
|
assertEquals(DeviceType.Tv, DeviceType.fromString(DeviceType.Tv.toString()));
|
|
assertEquals(DeviceType.Computer, DeviceType.fromString("invalid"));
|
|
}
|
|
|
|
// Basic paired device testing
|
|
@Test
|
|
public void testDevice() throws CertificateException {
|
|
Device device = new Device(context, "testDevice");
|
|
|
|
assertEquals(device.getDeviceId(), "testDevice");
|
|
assertEquals(device.getDeviceType(), DeviceType.Phone);
|
|
assertEquals(device.getName(), "Test Device");
|
|
assertTrue(device.isPaired());
|
|
assertNotNull(device.deviceInfo.certificate);
|
|
}
|
|
|
|
public void testPairingDone() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, CertificateException {
|
|
|
|
NetworkPacket fakeNetworkPacket = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY);
|
|
String deviceId = "unpairedTestDevice";
|
|
fakeNetworkPacket.set("deviceId", deviceId);
|
|
fakeNetworkPacket.set("deviceName", "Unpaired Test Device");
|
|
fakeNetworkPacket.set("protocolVersion", DeviceHelper.ProtocolVersion);
|
|
fakeNetworkPacket.set("deviceType", DeviceType.Phone.toString());
|
|
String certificateString =
|
|
"MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
|
|
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
|
|
"bmVjdDAeFw0xNTA2MDMxMzE0MzhaFw0yNTA2MDMxMzE0MzhaMFUxLzAtBgNVBAMMJl9kYTE3OWE5\n" +
|
|
"MV9mMDY0XzQ3OGVfYmU4Y18xOTM1ZDc1NDRkNTRfMQwwCgYDVQQKDANLREUxFDASBgNVBAsMC0tk\n" +
|
|
"ZSBjb25uZWN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzH9GxS1lctpwYdSGAoPH\n" +
|
|
"ws+MnVaL0PVDCuzrpxzXc+bChR87xofhQIesLPLZEcmUJ1MlEJ6jx4W+gVhvY2tUN7SoiKKbnq8s\n" +
|
|
"WjI5ovs5yML3C1zPbOSJAdK613FcdkK+UGd/9dQk54gIozinC58iyTAChVVpB3pAF38EPxwKkuo2\n" +
|
|
"qTzwk24d6PRxz1skkzwEphUQQzGboyHsAlJHN1MzM2/yFGB4l8iUua2d3ETyfy/xFEh/SwtGtXE5\n" +
|
|
"KLz4cpb0fxjeYQZVruBKxzE07kgDO3zOhmP3LJ/KSPHWYImd1DWmpY9iDvoXr6+V7FAnRloaEIyg\n" +
|
|
"7WwdlSCpo3TXVuIjLwIDAQABozIwMDAdBgNVHQ4EFgQUwmbHo8YbiR463GRKSLL3eIKyvDkwDwYD\n" +
|
|
"VR0TAQH/BAUwAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAydijH3rbnvpBDB/30w2PCGMT7O0N/XYM\n" +
|
|
"wBtUidqa4NFumJrNrccx5Ehp4UP66BfP61HW8h2U/EekYfOsZyyWd4KnsDD6ycR8h/WvpK3BC2cn\n" +
|
|
"I299wbqCEZmk5ZFFaEIDHdLAdgMCuxJkAzy9mMrWEa05Soxi2/ZXdrU9nXo5dzuPGYlirVPDHl7r\n" +
|
|
"/urBxD6HVX3ObQJRJ7r/nAWyUVdX3/biJaDRsydftOpGU6Gi5c1JK4MWIz8Bsjh6mEjCsVatbPPl\n" +
|
|
"yygGiJbDZfAvN2XoaVEBii2GDDCWfaFwPVPYlNTvjkUkMP8YThlMsiJ8Q4693XoLOL94GpNlCfUg\n" +
|
|
"7n+KOQ==";
|
|
byte[] certificateBytes = Base64.decode(certificateString, 0);
|
|
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
|
|
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(fakeNetworkPacket, certificate);
|
|
|
|
LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class);
|
|
Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider");
|
|
LanLink link = Mockito.mock(LanLink.class);
|
|
Mockito.when(link.getLinkProvider()).thenReturn(linkProvider);
|
|
Mockito.when(link.getDeviceId()).thenReturn(deviceId);
|
|
Mockito.when(link.getDeviceInfo()).thenReturn(deviceInfo);
|
|
Device device = new Device(context, link);
|
|
|
|
assertNotNull(device);
|
|
assertEquals(device.getDeviceId(), deviceId);
|
|
assertEquals(device.getName(), "Unpaired Test Device");
|
|
assertEquals(device.getDeviceType(), DeviceType.Phone);
|
|
assertNotNull(device.deviceInfo.certificate);
|
|
|
|
Method method = PairingHandler.class.getDeclaredMethod("pairingDone");
|
|
method.setAccessible(true);
|
|
method.invoke(device.pairingHandler);
|
|
|
|
assertTrue(device.isPaired());
|
|
|
|
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
|
assertTrue(preferences.getBoolean(device.getDeviceId(), false));
|
|
|
|
SharedPreferences settings = context.getSharedPreferences(device.getDeviceId(), Context.MODE_PRIVATE);
|
|
assertEquals(settings.getString("deviceName", "Unknown device"), "Unpaired Test Device");
|
|
assertEquals(settings.getString("deviceType", "tablet"), "phone");
|
|
|
|
// Cleanup for unpaired test device
|
|
preferences.edit().remove(device.getDeviceId()).apply();
|
|
settings.edit().clear().apply();
|
|
}
|
|
|
|
@Test
|
|
public void testUnpair() throws CertificateException {
|
|
PairingHandler.PairingCallback pairingCallback = Mockito.mock(PairingHandler.PairingCallback.class);
|
|
Device device = new Device(context, "testDevice");
|
|
device.addPairingCallback(pairingCallback);
|
|
|
|
device.unpair();
|
|
|
|
assertFalse(device.isPaired());
|
|
|
|
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
|
|
assertFalse(preferences.getBoolean(device.getDeviceId(), false));
|
|
|
|
Mockito.verify(pairingCallback, Mockito.times(1)).unpaired();
|
|
}
|
|
}
|