Skip to content

Commit

Permalink
Merge pull request #1602 from AzureAD/paul/fix-instrumented-tests
Browse files Browse the repository at this point in the history
Fix for mocking final classes in some instrumented tests
  • Loading branch information
paulkagiri authored Jul 26, 2021
2 parents 1ddf16e + bae680b commit 390c889
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 80 deletions.
6 changes: 6 additions & 0 deletions adal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ dependencies {

// Test Dependencies
testImplementation "junit:junit:$rootProject.ext.junitVersion"
//mockito-inline was introduced in mockito 2.7.6
//see: https://javadoc.io/static/org.mockito/mockito-core/3.6.28/org/mockito/Mockito.html#0.1
testImplementation "org.mockito:mockito-inline:$rootProject.ext.mockitoCoreVersion"
testImplementation ("org.robolectric:robolectric:$rootProject.ext.robolectricVersion")
testImplementation "androidx.test:core:$rootProject.ext.androidxTestCoreVersion"
testImplementation project(':testutils')

// Javadoc Dependencies
javadocDeps "androidx.annotation:annotation:$rootProject.ext.annotationVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2296,8 +2296,7 @@ private PackageManager getMockedPackageManager() throws PackageManager.NameNotFo
mockedSignature.toByteArray()
).thenReturn(Base64.decode(Util.ENCODED_SIGNATURE, Base64.NO_WRAP));

final PackageInfo mockedPackageInfo = Util.addSignatures(Mockito.mock(PackageInfo.class), new Signature[]{mockedSignature});

final PackageInfo mockedPackageInfo = new MockedPackageInfo(new Signature[]{mockedSignature});
final PackageManager mockedPackageManager = Mockito.mock(PackageManager.class);
when(
mockedPackageManager.getPackageInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,8 @@ private void mockPackageManagerBrokerSignatureAndPermission(final PackageManager
Mockito.when(packageManager.checkPermission(Mockito.contains("android.permission.GET_ACCOUNTS"),
Mockito.anyString())).thenReturn(PackageManager.PERMISSION_DENIED);

final PackageInfo packageInfo = Util.addSignatures(Mockito.mock(PackageInfo.class), new Signature[]{signature});

Mockito.when(packageManager.getPackageInfo(Mockito.anyString(), Mockito.anyInt())).thenReturn(packageInfo);
final PackageInfo mockedPackageInfo = new MockedPackageInfo(new Signature[]{signature});
Mockito.when(packageManager.getPackageInfo(Mockito.anyString(), Mockito.anyInt())).thenReturn(mockedPackageInfo);

Mockito.when(packageManager.checkPermission(Mockito.contains("android.permission.GET_ACCOUNTS"),
Mockito.anyString())).thenReturn(PackageManager.PERMISSION_DENIED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
Expand Down Expand Up @@ -89,7 +90,6 @@
import static org.mockito.Mockito.when;

@RunWith(AndroidJUnit4.class)
@Ignore("SigningInfo cannot be mocked. Disabled until that is fixed.")
public class BrokerProxyTests {

static final String TEST_AUTHORITY = "https://login.windows.net/common/";
Expand Down Expand Up @@ -1170,13 +1170,11 @@ private Context getMockContext(final Signature signature, final String brokerPac
return mockContext;
}

@SuppressLint("PackageManagerGetSignatures")
private PackageManager getPackageManager(final Signature signature, final String packageName,
boolean permissionStatus) throws NameNotFoundException {
PackageManager mockPackage = mock(PackageManager.class);
PackageInfo info = Util.addSignatures(new PackageInfo(), new Signature[]{signature});

when(mockPackage.getPackageInfo(packageName, PackageHelper.getPackageManagerSignaturesFlag())).thenReturn(info);
final PackageInfo mockedPackageInfo = new MockedPackageInfo(new Signature[]{signature});
when(mockPackage.getPackageInfo(packageName, PackageHelper.getPackageManagerSignaturesFlag())).thenReturn(mockedPackageInfo);
when(mockPackage.checkPermission(anyString(), anyString()))
.thenReturn(permissionStatus ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
return mockPackage;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package com.microsoft.aad.adal;

import android.content.pm.PackageInfo;
import android.content.pm.Signature;
import android.os.Build;

public class MockedPackageInfo extends PackageInfo {

public MockedSigningInfo signingInfo;

public MockedPackageInfo(Signature [] signatures) {
this.signingInfo = new MockedSigningInfo(signatures);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
this.signatures = signatures;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package com.microsoft.aad.adal;

import android.content.pm.Signature;

public class MockedSigningInfo {

private final Signature[] signatures;

public MockedSigningInfo(Signature[] signatures) {
this.signatures = signatures;
}

public boolean hasMultipleSigners() {
return signatures != null && signatures.length > 1;
}

public boolean hasPastSigningCertificates() {
return false;
}

public Signature[] getSigningCertificateHistory() {
return signatures;
}

public Signature[] getApkContentsSigners() {
return signatures;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
Expand All @@ -39,7 +38,6 @@

import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

Expand All @@ -57,7 +55,6 @@
import javax.crypto.spec.SecretKeySpec;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -112,37 +109,6 @@ public void tearDown() {
}

@Test
@Ignore("SigningInfo cannot be mocked. Disabled until that is fixed.")
public void testGetCurrentSignatureForPackage() throws NameNotFoundException,
IllegalArgumentException, ClassNotFoundException, NoSuchMethodException,
InstantiationException, IllegalAccessException, InvocationTargetException {
final Context mockContext = getMockContext(
new Signature(mTestSignature),
mContext.getPackageName(),
0 // calling uid
);
final Object packageHelper = getInstance(mockContext);
final Method m = ReflectionUtils.getTestMethod(
packageHelper,
"getCurrentSignatureForPackage", // method name
String.class
);

// act
String actual = (String) m.invoke(packageHelper, mContext.getPackageName());

// assert
assertEquals("should be same info", mTestTag, actual);

// act
actual = (String) m.invoke(packageHelper, (String) null);

// assert
assertNull("should return null", actual);
}

@Test
@Ignore("SigningInfo cannot be mocked. Disabled until that is fixed.")
public void testGetUIDForPackage() throws NameNotFoundException, IllegalArgumentException,
ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
Expand All @@ -169,7 +135,6 @@ public void testGetUIDForPackage() throws NameNotFoundException, IllegalArgument
}

@Test
@Ignore("SigningInfo cannot be mocked. Disabled until that is fixed.")
public void testRedirectUrl() throws NameNotFoundException, IllegalArgumentException,
ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException, UnsupportedEncodingException {
Expand Down Expand Up @@ -223,7 +188,7 @@ private PackageManager getPackageManager(final Signature signature,
final String packageName,
final int callingUID) throws NameNotFoundException {
final PackageManager mockPackage = mock(PackageManager.class);
final PackageInfo info = Util.addSignatures(new PackageInfo(), new Signature[]{signature});
final MockedPackageInfo mockedPackageInfo = new MockedPackageInfo(new Signature[]{signature});

final ApplicationInfo appInfo = new ApplicationInfo();
appInfo.name = packageName;
Expand All @@ -233,7 +198,7 @@ private PackageManager getPackageManager(final Signature signature,
packageName,
PackageHelper.getPackageManagerSignaturesFlag()
)
).thenReturn(info);
).thenReturn(mockedPackageInfo);

when(mockPackage.getApplicationInfo(packageName, 0)).thenReturn(appInfo);
Context mock = mock(Context.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import com.microsoft.identity.common.adal.internal.util.StringExtensions;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
Expand Down Expand Up @@ -64,7 +63,6 @@ public void setUp() throws Exception {
}

@Test
@Ignore("SigningInfo cannot be mocked. Disabled until that is fixed.")
public void testAggregatedDispatcher() throws PackageManager.NameNotFoundException {
final TestDispatcher dispatch = new TestDispatcher();
final AggregatedDispatcher dispatcher = new AggregatedDispatcher(dispatch);
Expand All @@ -86,7 +84,6 @@ public void testAggregatedDispatcher() throws PackageManager.NameNotFoundExcepti
}

@Test
@Ignore("SigningInfo cannot be mocked. Disabled until that is fixed.")
public void testDefaultDispatcher() throws PackageManager.NameNotFoundException {
final TestDispatcher dispatch = new TestDispatcher();
final DefaultDispatcher dispatcher = new DefaultDispatcher(dispatch);
Expand All @@ -110,8 +107,7 @@ private PackageManager getMockedPackageManager() throws PackageManager.NameNotFo
when(mockedSignature.toByteArray()).thenReturn(Base64.decode(
Util.ENCODED_SIGNATURE, Base64.NO_WRAP));

final PackageInfo mockedPackageInfo = Util.addSignatures(Mockito.mock(PackageInfo.class), new Signature[]{mockedSignature});

final PackageInfo mockedPackageInfo = new MockedPackageInfo(new Signature[]{mockedSignature});
final PackageManager mockedPackageManager = Mockito.mock(PackageManager.class);
when(mockedPackageManager.getPackageInfo(Mockito.anyString(), anyInt())).thenReturn(mockedPackageInfo);

Expand Down
27 changes: 0 additions & 27 deletions adal/src/androidTest/java/com/microsoft/aad/adal/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
// THE SOFTWARE.
package com.microsoft.aad.adal;

import android.content.pm.PackageInfo;
import android.content.pm.Signature;
import android.content.pm.SigningInfo;
import android.os.Build;
import android.util.Base64;

import com.microsoft.identity.common.adal.internal.AuthenticationConstants;
Expand Down Expand Up @@ -55,9 +51,6 @@
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

final class Util {
public static final int TEST_PASSWORD_EXPIRATION = 1387227772;
static final String TEST_IDTOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdWQiOiJlNzBiMTE1ZS1hYzBhLTQ4MjMtODVkYS04ZjRiN2I0ZjAwZTYiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8zMGJhYTY2Ni04ZGY4LTQ4ZTctOTdlNi03N2NmZDA5OTU5NjMvIiwibmJmIjoxMzc2NDI4MzEwLCJleHAiOjEzNzY0NTcxMTAsInZlciI6IjEuMCIsInRpZCI6IjMwYmFhNjY2LThkZjgtNDhlNy05N2U2LTc3Y2ZkMDk5NTk2MyIsIm9pZCI6IjRmODU5OTg5LWEyZmYtNDExZS05MDQ4LWMzMjIyNDdhYzYyYyIsInVwbiI6ImFkbWluQGFhbHRlc3RzLm9ubWljcm9zb2Z0LmNvbSIsInVuaXF1ZV9uYW1lIjoiYWRtaW5AYWFsdGVzdHMub25taWNyb3NvZnQuY29tIiwic3ViIjoiVDU0V2hGR1RnbEJMN1VWYWtlODc5UkdhZEVOaUh5LXNjenNYTmFxRF9jNCIsImZhbWlseV9uYW1lIjoiU2VwZWhyaSIsImdpdmVuX25hbWUiOiJBZnNoaW4ifQ.";
Expand Down Expand Up @@ -234,24 +227,4 @@ static Map<String, byte[]> getSecretKeys() throws NoSuchAlgorithmException, Inva
secretKeys.put(AuthenticationSettings.INSTANCE.getBrokerPackageName(), secretKey2.getEncoded());
return secretKeys;
}

/**
* Utility for adding signatures to a passed PackageInfo in a back-compatible way
*
* @param packageInfo to add signatures to
* @param signatures the signatures to add
* @return PackageInfo with signatures added
*/
public static PackageInfo addSignatures(final PackageInfo packageInfo, final Signature[] signatures) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
packageInfo.signatures = signatures;
return packageInfo;
}

final SigningInfo signingInfo = mock(SigningInfo.class);
when(signingInfo.hasMultipleSigners()).thenReturn(false);
when(signingInfo.getSigningCertificateHistory()).thenReturn(signatures);
packageInfo.signingInfo = signingInfo;
return packageInfo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package com.microsoft.aad.adal;

import android.content.pm.PackageInfo;
import android.content.pm.Signature;
import android.util.Base64;

import com.microsoft.identity.common.internal.broker.PackageHelper;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.security.MessageDigest;

import static org.junit.Assert.assertEquals;

@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, shadows = {
SigningInfoShadow.class
})
public class PackageHelperCurrentSignatureTests {
static final String ENCODED_SIGNATURE = "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";
private String testSignature;
private Signature mockedSignature;

@Before
public void setUp() throws Exception {
mockedSignature = new Signature(Base64.decode(ENCODED_SIGNATURE, Base64.NO_WRAP));
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(mockedSignature.toByteArray());
testSignature = Base64.encodeToString(md.digest(), Base64.NO_WRAP);
}

@Test
public void testGetCurrentSignatureForPackage() {
SigningInfoShadow.setSignatures(new Signature[]{mockedSignature});
PackageInfo packageInfo = new PackageInfo();
packageInfo.signatures = new Signature[]{mockedSignature};
String signature = PackageHelper.getCurrentSignatureForPackage(packageInfo);
// assert
assertEquals("should be same info", testSignature, signature);
}
}
Loading

0 comments on commit 390c889

Please sign in to comment.