@W-21148602: Internal Server List does not respect changes made to Servers.xml file#2845
Conversation
| Context.MODE_PRIVATE); | ||
|
|
||
| // Reset non-custom servers from mobile device management (MDM) and servers XML. | ||
| resetNonCustomLoginServers(runtimePrefs); |
There was a problem hiding this comment.
@wmathurin, @brandonpage and @bbirman, before I update the tests and move this into ready-for-review I wanted to run the concept of the change past you all.
I chatted briefly with @bbirman to review the iOS behavior since the work item specifically hails that logic as working compared to Android's. Currently, MSDK Android loads the servers.xml only once on install. The work item mentions upgrade, but I didn't see that logic. More, when MDM provides servers they're additive only. When MDM drops a server, the app doesn't reflect that.
What I'm trying here and seems to manually test well is:
- Drop both sets of non-custom servers on startup, including non-custom servers from
servers.xmland MDM. - Load the current set of non-custom servers from either
servers.xmlor MDM according to the existing logic, as much as possible. - When MDM is not enabled, preserve the user's custom login servers at the end of the list
I'm doing a little more testing around the behavior of the only show authorized hosts parameter. I want to be sure the MDM flow still respects the user's custom servers.
The question to answer is: Does this seem like a reasonable update to the existing logic to resolve this work item? I can manipulate servers.xml and the MDM data and the app seems to keep the servers list adds, updates or removes non-custom servers just as we'd want. The logic here is older and the methods are not as self-contained as our newer logic, so I wanted to be sure we all got to review any potential side-effects of this before moving further!
There was a problem hiding this comment.
For #3 (and pending your authorized hosts testing), on iOS the custom login servers would be preserved if MDM was enabled as long as "onlyShowAuthorizedHosts" is false
There was a problem hiding this comment.
This sounds good to me.
One thought though: Should we only remove MDM servers when network is available? Security wise, it would be bad it:
- Put device in airplane mode
- Open app. MDM servers are deleted and updated servers cannot be retrieved.
- Turn airplane mode off.
LoginServerManagerconstructor does not run again. MDM server list is not respected.
Is that scenario possible?
There was a problem hiding this comment.
Thanks and that thought about network is a great topic. I'll do some research!
There was a problem hiding this comment.
I took a manual look and also asked our agents. RuntimeConfig does cache the policy locally and will give MSDK those values when no network is present ✅
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #2845 +/- ##
============================================
+ Coverage 64.55% 64.84% +0.29%
- Complexity 2925 2962 +37
============================================
Files 222 222
Lines 17323 17365 +42
Branches 2471 2476 +5
============================================
+ Hits 11182 11261 +79
+ Misses 4934 4905 -29
+ Partials 1207 1199 -8
🚀 New features to boost your workflow:
|
be2307b to
f7984d3
Compare
…rvers.xml file (Greatly Improved Shared Preferences Data Integrity And Dynamic Handling Of RuntimeConfig. Removal Of To-Dos And Diagnostics Pending)
f7984d3 to
84a6441
Compare
…rvers.xml file (Remove Diagnostics And To-Dos)
…rvers.xml file (Slight Self-Review Cleanup, Revert `getLoginServersFromRuntimeConfig` To Maintain Point Release API Compatibility)
…rvers.xml file (Unit Test Updates)
Generated by 🚫 Danger |
…rvers.xml file (Unit Tests For Add, Update And Remove Servers Via Resources)
…rvers.xml file (Unit Tests For Add, Update And Remove Servers Via Runtime Config Mobile Device Management)
…rvers.xml file (Enhancements For Null Safety And Related Unit Tests)
3e8aa3f to
463b95c
Compare
…rvers.xml file (Additional Enhancements For Null Safety And Related Unit Tests)
…rvers.xml file (Improve Logic Branching In `getSelectedLoginServer`)
b58c7aa to
686e433
Compare
…rvers.xml file (Additional Tests And Improved Branching Logic In `removeServer`)
…rvers.xml file (Improved Null Handling In `getLoginServersFromRuntimeConfig`)
…rvers.xml file (Light Cleanup)
| public static final String WELCOME_LOGIN_URL = "https://welcome.salesforce.com/discovery"; | ||
| public static final String SANDBOX_LOGIN_URL = "https://test.salesforce.com"; | ||
|
|
||
| // Keys used in shared preferences. |
There was a problem hiding this comment.
I've reviewed the diff in Github once already, so my pre-amble is that Github does a terrible job (as it often does) correlating the diff. If one really wants to navigate the diff more easily, use Android Studio to compare the revisions 🎉
| private static final String IS_CUSTOM = "is_custom_%d"; | ||
| private static final String SERVER_SELECTION_FILE = "server_selection_file"; | ||
| /** | ||
| * Shared preferences when non-custom login servers are provided by resources servers.xml |
There was a problem hiding this comment.
There are some new documentation comments to help many of the less-intuitively named properties and methods be clearer and easily reviewed in Android Studio.
| /** | ||
| * Shared preferences when non-custom login servers are provided by resources servers.xml | ||
| */ | ||
| @VisibleForTesting |
There was a problem hiding this comment.
@VisibleForTesting appears in many places to enable the new tests.
| /** | ||
| * The Android runtime configuration used for Mobile Device Management (MDM) | ||
| */ | ||
| private final RuntimeConfig runtimeConfig; |
There was a problem hiding this comment.
Only two properties were added or modified: runtimeConfig is one and present for internal testing support.
| private final RuntimeConfig runtimeConfig; | ||
|
|
||
| /** The resource id of the servers.xml file */ | ||
| private final int serversXmlResourceId; |
There was a problem hiding this comment.
Only two properties were added or modified: serversXmlResourceId is one and present for internal testing support.
| * @param ctx The context | ||
| */ | ||
| @VisibleForTesting | ||
| public LoginServerManager( |
There was a problem hiding this comment.
This new and private parameterized constructor is present for testing dependency injection.
| runtimePrefs = ctx.getSharedPreferences(RUNTIME_PREFS_FILE, MODE_PRIVATE); | ||
|
|
||
| // (Re-)initialize non-custom login servers provided by the resources servers.xml. | ||
| resetNonCustomLoginServers(settings); |
There was a problem hiding this comment.
A crux change is here in the constructor to (re-)initialize the non-custom login servers from resources server.xml when applicable. Note, this doesn't affect the user's custom login servers.
| initSharedPrefFile(); | ||
|
|
||
| // Select a default login server. | ||
| getSelectedLoginServer(); |
There was a problem hiding this comment.
The updated getSelectedLoginServer has new logic to detect when the "selected" login server is no longer in the "available" login servers and select the new default login server.
| final boolean selectedServerHasValidNameAndUrl = selectedServerName != null && selectedServerUrl != null; | ||
|
|
||
| // Refresh the list of mobile device management (MDM) servers from the runtime config. | ||
| if (isRuntimeConfigAppServiceHostsSet()) { |
There was a problem hiding this comment.
This refresh of the runtime config login servers wasn't present before but now nicely compliments the following call to getLoginServers so the selected login server will reflect when MDM has removed the currently selected login server. This was an odd looking bug where the "selected" login server might no longer appear in the server picker after an MDM update.
| final List<LoginServer> loginServers = getLoginServers(); | ||
|
|
||
| // Check if the selected login server is available in the active list of login servers. | ||
| final boolean selectedLoginServerIsAvailable = loginServers.stream().anyMatch(server -> |
There was a problem hiding this comment.
Our agent tooling recommended this stream approach to search the list in Java. This'll be easier in Kotlin, someday.
| * @param url The login server URL | ||
| */ | ||
| public void addCustomLoginServer(String name, String url) { | ||
| public void addCustomLoginServer(@NonNull String name, @NonNull String url) { |
There was a problem hiding this comment.
Though it hadn't been reported, passing null for these parameters was a potential NullPointerException.
| } else { | ||
| persistLoginServer(name, url, true, runtimePrefs); | ||
| } | ||
| persistLoginServer( |
There was a problem hiding this comment.
The new getSharedPreferences() method lets this be one call to persistLoginServer with the correct SharedPreferences.
| public void removeServer(LoginServer server) { | ||
| List<LoginServer> servers = getLoginServers(); | ||
| @VisibleForTesting | ||
| public void removeServer( |
There was a problem hiding this comment.
This private and parameterized version of removeServer supports testing.
| allServers = getLoginServersFromPreferences(runtimePrefs); | ||
| } | ||
| return allServers; | ||
| return getLoginServersFromPreferences(getSharedPreferences()); |
There was a problem hiding this comment.
getLoginServers also gets a lot simpler since it can have one call to getLoginServersFromPreferences with the correct SharedPreferences parameter.
| final String url = mdmLoginServers[i]; | ||
|
|
||
| // Reset non-custom servers from Mobile Device Management (MDM). | ||
| resetNonCustomLoginServers(runtimePrefs); |
There was a problem hiding this comment.
Another crux change is here in getLoginServersFromRuntimeConfig to reset the non-custom login servers from that SharedPreferences so that strange things don't happen as the MDM data changes. My testing from dev showed that adding servers worked but deleting servers had no effect. This also meant that "editing" a server resulted in a duplication. The list always reflects MDM now.
|
|
||
| // Null-cleanse MDM login server URLs and names. | ||
| final List<String> mdmLoginServersList = asList(mdmLoginServers); | ||
| mdmLoginServersList.removeIf(Objects::isNull); |
There was a problem hiding this comment.
Our agent tools recommended this approach for null-cleansing the MDM data which aided in simplifying the tests and avoiding code coverage noise.
|
|
||
| // Edit each login server indexed after the updated index. | ||
| final Editor editor = settings.edit(); | ||
| final Editor editor = getSharedPreferences().edit(); |
There was a problem hiding this comment.
This was a latent bug where the wrong SharedPreferences was hard-coded even when MDM was active.
| * @param sharedPreferences The login server shared preferences | ||
| * @return The adjusted login server index | ||
| */ | ||
| private @NonNull Integer getIndexAdjustedToCustomLoginServerBounds( |
There was a problem hiding this comment.
These utility methods to adjust a login server's index to the appropriate bounds of non-custom or custom servers are useful, but for future when SharedPreferences is replaced it'd be amazing to see the custom servers stored separately from the non-custom ones entirely.
| eventType = xml.next(); | ||
| } catch (Exception e) { | ||
| SalesforceSDKLogger.w(TAG, "Exception thrown while parsing XML", e); | ||
| break; |
There was a problem hiding this comment.
Without this break, this exception turned into infinite recursion and an app hang which I caught in testing while adding coverage to this (previously?) untested line.
| edit.putString(String.format(Locale.US, SERVER_URL, i), curServer.url.trim()); | ||
| edit.putBoolean(String.format(Locale.US, IS_CUSTOM, i), curServer.isCustom); | ||
| if (i == 0) { | ||
| persistLoginServer( |
There was a problem hiding this comment.
All login server persistence happens now via persistLoginServer since it has all the logic needed to correctly order and sanitize the SharedPreferences.
| setSelectedLoginServer(curServer); | ||
| } | ||
| } | ||
| edit.putInt(NUMBER_OF_ENTRIES, numServers); |
There was a problem hiding this comment.
This NUMBER_OF_ENTRIES update happens in persistLoginServer now.
| private void persistLoginServer(String name, String url, boolean isCustom, SharedPreferences sharedPrefs) { | ||
| if (name == null || url == null) { | ||
| return; | ||
| private void persistLoginServer(final @NonNull String name, |
There was a problem hiding this comment.
Another big crux change is here to persistLoginServer which now has the ability to correctly order non-custom and custom login servers and all the other login servers. This is where the most work actually happened to make the non-custom login server list mutable.
| int numServers = prefs.getInt(NUMBER_OF_ENTRIES, 0); | ||
| if (numServers == 0) { | ||
| return null; | ||
| return new ArrayList<>(); |
There was a problem hiding this comment.
I left most other methods returning null if the did on dev but this one actually made sense to eliminate the null checks at all the private call sites and simplify the logic and tests.
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| @SmallTest | ||
| class LoginServerManagerTestKt { |
There was a problem hiding this comment.
Where mockk was needed, I wrote new tests directly in Kotlin.
There was a problem hiding this comment.
Maybe rename this one to LoginServerManagerMockTest?
At the very least let's make the file name match the class so we don't have LoginServerManageTest.java and LoginServerManageTest.kt.
|
Testing Notes: I tested this via both manual testing on device and via all the new unit tests. In particular, I also used a temporary method to log the contents of both Testing requires restarting the app after:
Each of these tests should be tested with a cold start of the app to ensure the (re-)initialization of the Each of these tests should be performed with and without the presence of custom login servers. The unit tests provide more destructive testing, such as inserting null values and malformed XML in all kinds of places they shouldn't be. I spent five days in manual testing and automated testing development since I'd view this as a higher risk change. This was where I found and resolved a number of |
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| @SmallTest | ||
| class LoginServerManagerTestKt { |
There was a problem hiding this comment.
Maybe rename this one to LoginServerManagerMockTest?
At the very least let's make the file name match the class so we don't have LoginServerManageTest.java and LoginServerManageTest.kt.
…rvers.xml file (Rename Kotlin/Mockk Tests For `LoginServerManager` To `LoginServerManagerMockTest.kt`)
🎸 Ready For Review 🥁
This updates the login server manager to respond to updates made to the login servers from servers.xml in resources and the login servers provided by Mobile Device Management via the runtime configuration. Previously, login servers from these two locations were static (for servers.xml) or additions-only (for runtime config) after the app had been installed.
For a point release, these changes do need to maintain compatibility with the 13.x public API. To minimize regression risk, they're also maintaining the Java style and deprecated shared preferences persistence already in place.
Accomplishing the change is non-trivial. The current logic was not built with mutability in mind for the non-custom servers. Scaling the existing logic and ensuring the shared preferences data maintains exact integrity did increase the change footprint.
New unit tests provide functional automated testing of all the changes and 100% patch code coverage. The overall test coverage for the login server manager is much, much higher than before as well.
I'll follow-up with inline source commentary as well. Thanks for reading!