Add tests for loading Classic accounts.
Tidy up warnings and fix a few bugs that were revealed.
This commit is contained in:
BIN
DevelopmentAssets/ClassicAppFixtures/accountsV2
Normal file
BIN
DevelopmentAssets/ClassicAppFixtures/accountsV2
Normal file
Binary file not shown.
BIN
DevelopmentAssets/ClassicAppFixtures/users/94
Normal file
BIN
DevelopmentAssets/ClassicAppFixtures/users/94
Normal file
Binary file not shown.
@@ -193,6 +193,7 @@
|
||||
1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; };
|
||||
1DC227816777A2F3A19657E5 /* RoomDirectorySearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */; };
|
||||
1ECE584D2CFA0FEF0ADE458C /* TestablePreviewsDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA723686F23EF45E2B398FBC /* TestablePreviewsDictionary.swift */; };
|
||||
1F1BCEE81056FD9F344F3B0E /* ClassicAppAccountManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80935ADC7ED867226225F965 /* ClassicAppAccountManagerTests.swift */; };
|
||||
1F3232BD368DF430AB433907 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; };
|
||||
1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; };
|
||||
1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; };
|
||||
@@ -847,6 +848,7 @@
|
||||
92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; };
|
||||
9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */; };
|
||||
9295F1F5E04484E10780BCE8 /* CharacterSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8C01DEEA83903D45069BBD /* CharacterSet.swift */; };
|
||||
92A0309B6F490B86C10B603B /* accountsV2 in Resources */ = {isa = PBXBuildFile; fileRef = 2A7BE2B89310058659E6F459 /* accountsV2 */; };
|
||||
92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; };
|
||||
92FE657CDFAFE3031576EB43 /* LinkNewDeviceScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED096460D7F26F10168FA33B /* LinkNewDeviceScreenViewModelProtocol.swift */; };
|
||||
9312F5A17AE59A9E910C51D6 /* NotificationItemProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840182D7A61402D5947DE094 /* NotificationItemProxyMock.swift */; };
|
||||
@@ -1061,6 +1063,7 @@
|
||||
B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; };
|
||||
B79E8AB83EBBDCD476D0362F /* PollFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622EC7898469BB1D0881CDD /* PollFormScreen.swift */; };
|
||||
B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28146817C61423CACCF942F5 /* CallScreenModels.swift */; };
|
||||
B7DA25BCB68A38AD903AEB06 /* 94 in Resources */ = {isa = PBXBuildFile; fileRef = 1BC752C2A4606C4C2D1ADB41 /* 94 */; };
|
||||
B7F58D6903F9D509EDAB9E4F /* MediaEventsTimelineScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033218DA395B003F7AB29A2 /* MediaEventsTimelineScreenModels.swift */; };
|
||||
B81840E45D8746A4692DA774 /* Tracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B574805B9812C111D6215D /* Tracing.swift */; };
|
||||
B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */; };
|
||||
@@ -1720,6 +1723,7 @@
|
||||
1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenModels.swift; sourceTree = "<group>"; };
|
||||
1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = "<group>"; };
|
||||
1BA8082E26C77A2C587B34B3 /* MockTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTimelineController.swift; sourceTree = "<group>"; };
|
||||
1BC752C2A4606C4C2D1ADB41 /* 94 */ = {isa = PBXFileReference; path = 94; sourceTree = "<group>"; };
|
||||
1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = "<group>"; };
|
||||
1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
|
||||
1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProvider.swift; sourceTree = "<group>"; };
|
||||
@@ -1809,6 +1813,7 @@
|
||||
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = "<group>"; };
|
||||
2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewModels.swift; sourceTree = "<group>"; };
|
||||
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
|
||||
2A7BE2B89310058659E6F459 /* accountsV2 */ = {isa = PBXFileReference; path = accountsV2; sourceTree = "<group>"; };
|
||||
2A95C9B8299A36A6495DECA6 /* TracingHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingHook.swift; sourceTree = "<group>"; };
|
||||
2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = "<group>"; };
|
||||
2ADF12A50186B75C68017B61 /* DeclineAndBlockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
@@ -1987,7 +1992,7 @@
|
||||
4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = "<group>"; };
|
||||
4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = "<group>"; };
|
||||
4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = wrapper.icon; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||
4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; path = AppIcon.icon; sourceTree = "<group>"; };
|
||||
4B2B564CA6570E1487A7C7CC /* SpaceRoomListProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxy.swift; sourceTree = "<group>"; };
|
||||
4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
|
||||
@@ -2253,6 +2258,7 @@
|
||||
7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; };
|
||||
7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = "<group>"; };
|
||||
8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = "<group>"; };
|
||||
80935ADC7ED867226225F965 /* ClassicAppAccountManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppAccountManagerTests.swift; sourceTree = "<group>"; };
|
||||
80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = "<group>"; };
|
||||
80F04B12FA231E797B7151A8 /* LinkNewDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceTests.swift; sourceTree = "<group>"; };
|
||||
810133CF215075C285FC6F3A /* test_image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = test_image.png; sourceTree = "<group>"; };
|
||||
@@ -3990,13 +3996,6 @@
|
||||
path = SupportingFiles;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4001FC7CD65776AB3745245C /* ClassicAppAccountConfirmationScreen */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = ClassicAppAccountConfirmationScreen;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4044C040B64B9F077298C947 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -4791,6 +4790,7 @@
|
||||
7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */,
|
||||
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */,
|
||||
0328F54E0C3AAEDDF3E05D9D /* ChatsTabFlowCoordinatorTests.swift */,
|
||||
80935ADC7ED867226225F965 /* ClassicAppAccountManagerTests.swift */,
|
||||
D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */,
|
||||
CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */,
|
||||
69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */,
|
||||
@@ -5783,6 +5783,7 @@
|
||||
A8002CB4F20B6282850A614C /* DevelopmentAssets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B3C5F21498CA4E4F255D596B /* ClassicAppFixtures */,
|
||||
DDAF2AD15C368A2809857B6A /* Media */,
|
||||
);
|
||||
path = DevelopmentAssets;
|
||||
@@ -5847,6 +5848,14 @@
|
||||
path = NSE;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B0863C1541E34EBE22549125 /* users */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1BC752C2A4606C4C2D1ADB41 /* 94 */,
|
||||
);
|
||||
path = users;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B1FC81662045E2369B0C4A0E /* Other */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -5903,6 +5912,15 @@
|
||||
path = SettingsScreen;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B3C5F21498CA4E4F255D596B /* ClassicAppFixtures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2A7BE2B89310058659E6F459 /* accountsV2 */,
|
||||
B0863C1541E34EBE22549125 /* users */,
|
||||
);
|
||||
path = ClassicAppFixtures;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B470504BE2DC95FAC94FDD79 /* ReadReceipts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -6618,7 +6636,6 @@
|
||||
children = (
|
||||
92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */,
|
||||
9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */,
|
||||
4001FC7CD65776AB3745245C /* ClassicAppAccountConfirmationScreen */,
|
||||
90F48FEF84016ED42A94BA24 /* LoginScreen */,
|
||||
BA1938A75D8C780F694CEB62 /* ServerConfirmationScreen */,
|
||||
2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */,
|
||||
@@ -7198,6 +7215,7 @@
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
@@ -7273,7 +7291,6 @@
|
||||
C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 681566846AF307E9BA4C72C6 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
@@ -7364,6 +7381,8 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B7DA25BCB68A38AD903AEB06 /* 94 in Resources */,
|
||||
92A0309B6F490B86C10B603B /* accountsV2 in Resources */,
|
||||
F252C0EA49088801F4CA6006 /* landscape_test_image.jpg in Resources */,
|
||||
F4582042AA4225CC1E4B8A1E /* landscape_test_video.mov in Resources */,
|
||||
8F3AD08F2E706AA60F1A1D4D /* portrait_test_image.jpg in Resources */,
|
||||
@@ -7667,6 +7686,7 @@
|
||||
1B2F9F368619FFF8C63C87CC /* BugReportScreenViewModelTests.swift in Sources */,
|
||||
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
|
||||
4BD5AB54A6982CF19F5CC7C4 /* ChatsTabFlowCoordinatorTests.swift in Sources */,
|
||||
1F1BCEE81056FD9F344F3B0E /* ClassicAppAccountManagerTests.swift in Sources */,
|
||||
B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */,
|
||||
3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */,
|
||||
0C932A5158C1D0604DFC5750 /* ComposerToolbarViewModelTests.swift in Sources */,
|
||||
|
||||
@@ -605,7 +605,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
private func startAuthentication() {
|
||||
let encryptionKeyProvider = EncryptionKeyProvider()
|
||||
let classicAppManager = makeClassicAppManager()
|
||||
let classicAppManager = ClassicAppManager()
|
||||
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
|
||||
encryptionKeyProvider: encryptionKeyProvider,
|
||||
classicAppManager: classicAppManager,
|
||||
@@ -630,19 +630,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
}
|
||||
}
|
||||
|
||||
private func makeClassicAppManager() -> ClassicAppManagerProtocol? {
|
||||
guard let classicAppGroupIdentifier = InfoPlistReader.main.classicAppGroupIdentifier,
|
||||
let classicAppKeychainServiceIdentifier = InfoPlistReader.main.classicAppKeychainServiceIdentifier,
|
||||
let classicAppKeychainAccessGroupIdentifier = InfoPlistReader.main.classicAppKeychainAccessGroupIdentifier else {
|
||||
MXLog.info("Classic App IDs not set, manager disabled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
return ClassicAppManager(classicAppGroupIdentifier: classicAppGroupIdentifier,
|
||||
classicAppKeychainServiceIdentifier: classicAppKeychainServiceIdentifier,
|
||||
classicAppKeychainAccessGroupIdentifier: classicAppKeychainAccessGroupIdentifier)
|
||||
}
|
||||
|
||||
private func runPostSessionSetupTasks() async {
|
||||
guard let userSession, let userSessionFlowCoordinator else {
|
||||
fatalError("User session not setup")
|
||||
@@ -675,7 +662,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
|
||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||
classicAppManager: makeClassicAppManager(),
|
||||
classicAppManager: ClassicAppManager(),
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
_ = await authenticationService.configure(for: userSession.clientProxy.homeserver, flow: .login)
|
||||
|
||||
@@ -25,16 +25,16 @@ class ClassicAppAccountManager {
|
||||
func loadAccounts() {
|
||||
MXLog.info("Loading accounts from Classic app.")
|
||||
|
||||
// First we need to load the accounts file, which contains an array of MXKAccounts (a super class of MXAccount).
|
||||
let accountFile = accountFile()
|
||||
if FileManager.default.fileExists(atPath: accountFile.path(percentEncoded: false)) {
|
||||
let startDate = Date()
|
||||
|
||||
do {
|
||||
let fileContent = try Data(contentsOf: accountFile, options: [.alwaysMapped, .uncached])
|
||||
|
||||
// Decrypt data if encryption method is provided
|
||||
let unciphered = try ClassicAppAES.decrypt(fileContent, aesKey: aesKey, iv: iv)
|
||||
let decoder = NSKeyedUnarchiver(forReadingWith: unciphered)
|
||||
let decoder = try NSKeyedUnarchiver(forReadingFrom: unciphered)
|
||||
decoder.requiresSecureCoding = false
|
||||
decoder.setClass(ClassicAppMXAccount.self, forClassName: "MXKAccount")
|
||||
|
||||
guard let mxAccounts = decoder.decodeObject(forKey: "mxAccounts") as? [ClassicAppMXAccount] else {
|
||||
@@ -44,6 +44,8 @@ class ClassicAppAccountManager {
|
||||
|
||||
MXLog.info("\(mxAccounts.count) accounts loaded in \(Date().timeIntervalSince(startDate) * 1000)ms.")
|
||||
|
||||
// Only consider active accounts using the same logic as Element Classic and then
|
||||
// combine the MXAccount data with its profile and crypto store details.
|
||||
accounts = mxAccounts
|
||||
.filter(\.isActive)
|
||||
.compactMap(makeAccount)
|
||||
@@ -57,9 +59,10 @@ class ClassicAppAccountManager {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Combines an MXAccount with its profile and crypto store details.
|
||||
private func makeAccount(for mxAccount: ClassicAppMXAccount) -> ClassicAppAccount? {
|
||||
let userID = mxAccount.userID
|
||||
let user = loadUser(for: mxAccount)
|
||||
let user = loadUser(for: mxAccount) // We need an MXUser for the profile as MXAccount doesn't contain that data.
|
||||
|
||||
guard let serverName = serverName(for: userID) else { return nil }
|
||||
|
||||
@@ -72,13 +75,21 @@ class ClassicAppAccountManager {
|
||||
}
|
||||
|
||||
private func loadUser(for mxAccount: ClassicAppMXAccount) -> ClassicAppMXUser? {
|
||||
// Users are stored across multiple files, so first find the right file for this particular user.
|
||||
let userID = mxAccount.userID
|
||||
let groupID = String(userID.hash % 100)
|
||||
let groupFile = storeUsersPath(for: userID).appendingPathComponent(groupID)
|
||||
let groupID = String(UInt(bitPattern: userID.hash) % 100) // Swift's .hash is Int whereas Objective-C's is UInt.
|
||||
let groupFile = storeUsersPath(for: userID).appending(component: groupID)
|
||||
|
||||
guard FileManager.default.fileExists(atPath: groupFile.path(percentEncoded: false)) else {
|
||||
MXLog.warning("Missing users group \(groupID) file for \(mxAccount.userID).")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
// And then load that file and find the user within its data.
|
||||
let fileContent = try Data(contentsOf: groupFile)
|
||||
let decoder = NSKeyedUnarchiver(forReadingWith: fileContent)
|
||||
let decoder = try NSKeyedUnarchiver(forReadingFrom: fileContent)
|
||||
decoder.requiresSecureCoding = false
|
||||
decoder.setClass(ClassicAppMXUser.self, forClassName: "MXUser")
|
||||
decoder.setClass(ClassicAppMXUser.self, forClassName: "MXMyUser")
|
||||
|
||||
@@ -92,7 +103,6 @@ class ClassicAppAccountManager {
|
||||
|
||||
/// The server name extracted from the user's ID.
|
||||
private func serverName(for userID: String) -> String? {
|
||||
#warning("Expose a serverName method for this from the SDK?")
|
||||
let components = userID.components(separatedBy: ":")
|
||||
guard components.count > 1 else { return nil }
|
||||
return components[1] // Directly use [1] as .last may be the port number.
|
||||
@@ -107,17 +117,17 @@ class ClassicAppAccountManager {
|
||||
private static let kMXFileStoreUsersFolder = "users"
|
||||
|
||||
/// The file URL that contains the app's `MXAccounts` array.
|
||||
private func accountFile() -> URL {
|
||||
func accountFile() -> URL {
|
||||
cacheFolder.appending(component: Self.matrixKitFolder).appending(component: Self.kMXKAccountsKey)
|
||||
}
|
||||
|
||||
/// The database file URL as defined in `MXCryptoMachineStore`.
|
||||
private func cryptoStoreURL(for userID: String) -> URL {
|
||||
func cryptoStoreURL(for userID: String) -> URL {
|
||||
cacheFolder.appending(component: Self.cryptoStoreFolder).appending(component: userID)
|
||||
}
|
||||
|
||||
/// This store contains all of the users known to the specific user ID.
|
||||
private func storeUsersPath(for userID: String) -> URL {
|
||||
func storeUsersPath(for userID: String) -> URL {
|
||||
storePath(for: userID).appending(component: Self.kMXFileStoreUsersFolder)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ClassicAppAccount {
|
||||
struct ClassicAppAccount: Equatable {
|
||||
let userID: String
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
@@ -18,7 +18,7 @@ struct ClassicAppAccount {
|
||||
|
||||
// MARK: NSCoding Types
|
||||
|
||||
class ClassicAppMXAccount: NSObject, NSCoding {
|
||||
final class ClassicAppMXAccount: NSObject, NSCoding {
|
||||
/// The obtained user ID.
|
||||
var userID: String
|
||||
/// The homeserver url (ex: "https://matrix.org").
|
||||
@@ -83,7 +83,7 @@ class ClassicAppMXAccount: NSObject, NSCoding {
|
||||
}
|
||||
|
||||
/// `MXUser` represents a user in Matrix.
|
||||
class ClassicAppMXUser: NSObject, NSCoding {
|
||||
final class ClassicAppMXUser: NSObject, NSCoding {
|
||||
/// The user id.
|
||||
let userID: String
|
||||
/// The user display name.
|
||||
|
||||
@@ -19,6 +19,7 @@ enum ClassicAppManagerError: Error {
|
||||
case missingCryptoStorePassphrase
|
||||
}
|
||||
|
||||
/// Reads accounts from Element Classic's shared storage.
|
||||
final class ClassicAppManager: ClassicAppManagerProtocol {
|
||||
private enum KeychainKeys: String {
|
||||
case cryptoSDKStoreKey
|
||||
@@ -29,16 +30,28 @@ final class ClassicAppManager: ClassicAppManagerProtocol {
|
||||
private let classicAppGroupIdentifier: String
|
||||
private let keychain: Keychain
|
||||
|
||||
init(classicAppGroupIdentifier: String, classicAppKeychainServiceIdentifier: String, classicAppKeychainAccessGroupIdentifier: String) {
|
||||
/// Creates an instance using the Classic app identifiers specified in the `Info.plist` file.
|
||||
/// Returns `nil` when a Classic app has not been configured in the project.
|
||||
init?(classicAppGroupIdentifier: String? = InfoPlistReader.main.classicAppGroupIdentifier,
|
||||
classicAppKeychainServiceIdentifier: String? = InfoPlistReader.main.classicAppKeychainServiceIdentifier,
|
||||
classicAppKeychainAccessGroupIdentifier: String? = InfoPlistReader.main.classicAppKeychainAccessGroupIdentifier) {
|
||||
guard let classicAppGroupIdentifier, let classicAppKeychainServiceIdentifier, let classicAppKeychainAccessGroupIdentifier else {
|
||||
MXLog.info("Classic App IDs not available, skipping initialisation.")
|
||||
return nil
|
||||
}
|
||||
|
||||
self.classicAppGroupIdentifier = classicAppGroupIdentifier
|
||||
keychain = Keychain(service: classicAppKeychainServiceIdentifier, accessGroup: classicAppKeychainAccessGroupIdentifier)
|
||||
}
|
||||
|
||||
/// Loads all of the active accounts from the Classic app.
|
||||
func loadAccounts() throws -> [ClassicAppAccount] {
|
||||
// The account data is stored in the App Group container.
|
||||
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: classicAppGroupIdentifier) else {
|
||||
throw ClassicAppManagerError.invalidAppGroupIdentifier(classicAppGroupIdentifier)
|
||||
}
|
||||
|
||||
// And the data is encrypted with keys that are stored in the Keychain.
|
||||
guard let accountIV = try keychain.getData(KeychainKeys.accountIV.rawValue),
|
||||
let accountAESKey = try keychain.getData(KeychainKeys.accountAESKey.rawValue) else {
|
||||
throw ClassicAppManagerError.missingAccountKeys
|
||||
|
||||
83
UnitTests/Sources/ClassicAppAccountManagerTests.swift
Normal file
83
UnitTests/Sources/ClassicAppAccountManagerTests.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
final class ClassicAppAccountManagerTests {
|
||||
let testDirectory: URL = .temporaryDirectory.appending(component: UUID().uuidString)
|
||||
let accountAESKey: Data
|
||||
let accountIV: Data
|
||||
let cryptoStorePassphrase: Data
|
||||
|
||||
let classicAppAccountManager: ClassicAppAccountManager
|
||||
|
||||
init() throws {
|
||||
accountAESKey = try #require(Data(base64Encoded: "BzaSCm5i8QhJr6wGBPj7MDvqBwkwuHLqkRxprVV2zJE="))
|
||||
accountIV = try #require(Data(base64Encoded: "dMmg6H2dYTRBE8PwjfAbAQ=="))
|
||||
cryptoStorePassphrase = try #require(Data(base64Encoded: "ERE/ZXw8rlY3Lv3MBG9sV9g+euOuJnrJaSuvrAMWPrI="))
|
||||
|
||||
classicAppAccountManager = ClassicAppAccountManager(cacheFolder: testDirectory,
|
||||
aesKey: accountAESKey,
|
||||
iv: accountIV,
|
||||
cryptoStorePassphrase: cryptoStorePassphrase)
|
||||
}
|
||||
|
||||
@Test
|
||||
func noAccounts() {
|
||||
classicAppAccountManager.loadAccounts()
|
||||
|
||||
#expect(classicAppAccountManager.accounts.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func activeAccount() throws {
|
||||
let account = ClassicAppAccount.mock(classicAppAccountManager: classicAppAccountManager,
|
||||
cryptoStorePassphrase: cryptoStorePassphrase)
|
||||
|
||||
try setupFixtures(for: account)
|
||||
|
||||
classicAppAccountManager.loadAccounts()
|
||||
|
||||
#expect(classicAppAccountManager.accounts.count == 1)
|
||||
#expect(classicAppAccountManager.accounts.first == account)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupFixtures(for account: ClassicAppAccount) throws {
|
||||
let bundle = Bundle(for: Self.self)
|
||||
|
||||
// Copy the accountsV2 file (contains the MXKAccount for the signed in user).
|
||||
let accountFileSource = try #require(bundle.url(forResource: "accountsV2", withExtension: nil))
|
||||
let accountFileDestination = classicAppAccountManager.accountFile()
|
||||
try FileManager.default.createDirectory(at: accountFileDestination.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
try FileManager.default.copyItem(at: accountFileSource, to: accountFileDestination)
|
||||
|
||||
// Copy the required users file (contains a subset of known MXUsers including the signed in user).
|
||||
let userFileName = "94" // UInt(bitPattern: account.userID.hash) % 100
|
||||
let userFileSource = try #require(bundle.url(forResource: userFileName, withExtension: nil))
|
||||
let usersDestination = classicAppAccountManager.storeUsersPath(for: account.userID)
|
||||
try FileManager.default.createDirectory(at: usersDestination, withIntermediateDirectories: true)
|
||||
try FileManager.default.copyItem(at: userFileSource, to: usersDestination.appending(component: userFileName))
|
||||
}
|
||||
}
|
||||
|
||||
extension ClassicAppAccount {
|
||||
/// Creates a mock account based on the fixtures used by this test.
|
||||
static func mock(classicAppAccountManager: ClassicAppAccountManager, cryptoStorePassphrase: Data) -> ClassicAppAccount {
|
||||
let userID = "@classicappaccount:matrix.org"
|
||||
|
||||
return ClassicAppAccount(userID: userID,
|
||||
displayName: "Classic App Account",
|
||||
avatarURL: "mxc://matrix.org/LYIzLOiILkjQJCqsgzAOUirs",
|
||||
serverName: "matrix.org",
|
||||
cryptoStoreURL: classicAppAccountManager.cryptoStoreURL(for: userID),
|
||||
cryptoStorePassphrase: cryptoStorePassphrase)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,9 @@ targets:
|
||||
sources:
|
||||
- path: ../Sources
|
||||
- path: ../SupportingFiles
|
||||
- path: ../../DevelopmentAssets
|
||||
- path: ../../DevelopmentAssets/Media
|
||||
- path: ../../DevelopmentAssets/ClassicAppFixtures
|
||||
buildPhase: resources # Necessary to add binary files to the target.
|
||||
- path: ../../ElementX/Sources/Other/Extensions/Publisher.swift
|
||||
- path: ../../ElementX/Sources/Other/Extensions/XCTestCase.swift
|
||||
- path: ../../ElementX/Sources/Other/InfoPlistReader.swift
|
||||
|
||||
Reference in New Issue
Block a user