MDM configuration & Unit/UI tests

This commit is contained in:
Christophe Deschamps 2026-04-16 02:51:29 +02:00 committed by Benoit Martins
parent e6b1b632d4
commit 0feac76611
18 changed files with 1549 additions and 21 deletions

View file

@ -65,7 +65,60 @@ class CoreContext: ObservableObject {
do {
try initialiseCore()
} catch {
}
observeMDMNotifications()
}
// MARK: - MDM notifications
private func observeMDMNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(onMDMConfigurationApplied(_:)), name: MDMManager.configurationAppliedNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onMDMConfigurationRemoved), name: MDMManager.configurationRemovedNotification, object: nil)
}
@objc private func onMDMConfigurationApplied(_ notification: Notification) {
Log.info("[CoreContext] MDM configuration applied, refreshing configuration")
CoreContext.shared.performActionOnCoreQueueWhenCoreIsStarted { core in
self.handleConfigurationChanged(status: .Successful)
}
}
@objc private func onMDMConfigurationRemoved() {
doOnCoreQueue { core in
Log.info("[CoreContext] MDM configuration removed, re-initializing app to default state")
var startCore = false
if core.globalState == .On {
core.stop()
startCore = true
}
AppServices.resetConfig()
self.mCore.config?.reload()
if startCore {
try?core.start()
}
self.handleConfigurationChanged(status: .Successful)
}
}
/// Shared handler for configuration changes (both from core provisioning and MDM).
private func handleConfigurationChanged(status: ConfiguringState) {
let themeMainColor = AppServices.corePreferences.themeMainColor
SharedMainViewModel.shared.updateConfigChanges()
if status == .Successful {
var accountModels: [AccountModel] = []
for account in self.mCore.accountList {
accountModels.append(AccountModel(account: account, core: self.mCore))
}
DispatchQueue.main.async {
self.accounts = accountModels
if accountModels.isEmpty {
self.loggingInProgress = false
self.loggedIn = false
}
ThemeManager.shared.applyTheme(named: themeMainColor)
self.reloadID = UUID()
}
}
}
@ -143,8 +196,14 @@ class CoreContext: ObservableObject {
Log.info("Found existing linphonerc file, skip copying of linphonerc-default configuration")
}
}
MDMManager.shared.loadXMLConfigFromMdm(config: AppServices.config)
self.mCore = try? Factory.Instance.createSharedCoreWithConfig(config: AppServices.config, systemContext: Unmanaged.passUnretained(coreQueue).toOpaque(), appGroupId: SharedMainViewModel.appGroupName, mainCore: true)
MDMManager.shared.applyMdmConfigToCore(core: self.mCore)
self.startObservingMDMConfigurationUpdates()
self.mCore.callkitEnabled = true
self.mCore.pushNotificationEnabled = true
@ -348,19 +407,7 @@ class CoreContext: ObservableObject {
}
}, onConfiguringStatus: { (_: Core, status: ConfiguringState, message: String) in
Log.info("New configuration state is \(status) = \(message)\n")
let themeMainColor = AppServices.corePreferences.themeMainColor
SharedMainViewModel.shared.updateConfigChanges()
DispatchQueue.main.async {
if status == ConfiguringState.Successful {
var accountModels: [AccountModel] = []
for account in self.mCore.accountList {
accountModels.append(AccountModel(account: account, core: self.mCore))
}
self.accounts = accountModels
ThemeManager.shared.applyTheme(named: themeMainColor)
self.reloadID = UUID()
}
}
self.handleConfigurationChanged(status: status)
}, onLogCollectionUploadStateChanged: { (_: Core, _: Core.LogCollectionUploadState, info: String) in
if info.starts(with: "https") {
DispatchQueue.main.async {
@ -563,6 +610,24 @@ class CoreContext: ObservableObject {
}
}
}
func startObservingMDMConfigurationUpdates() {
NotificationCenter.default.addObserver(
self,
selector: #selector(mdmConfigDidChange),
name: UserDefaults.didChangeNotification,
object: nil
)
}
@objc private func mdmConfigDidChange() {
guard MDMManager.shared.managedConfigChangedSinceLastCheck() else { return }
CoreContext.shared.doOnCoreQueue { core in
MDMManager.shared.applyMdmConfigToCore(core: core)
}
}
}
enum AppServices {
@ -589,8 +654,30 @@ enum AppServices {
}
return config
}
static func resetConfig() {
if let rcDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: SharedMainViewModel.appGroupName)?
.appendingPathComponent("Library/Preferences/linphone") {
let rcFileUrl = rcDir.appendingPathComponent("linphonerc")
do {
try FileManager.default.createDirectory(at: rcDir, withIntermediateDirectories: true)
if let pathToDefaultConfig = Bundle.main.path(forResource: "linphonerc-default", ofType: nil) {
if FileManager.default.fileExists(atPath: rcFileUrl.path) {
try FileManager.default.removeItem(at: rcFileUrl)
}
try FileManager.default.copyItem(at: URL(fileURLWithPath: pathToDefaultConfig), to: rcFileUrl)
Log.info("Successfully copied linphonerc-default configuration")
_config = nil
let _ = configIfAvailable
corePreferences = CorePreferences(config: config)
}
} catch let error {
Log.error("Failed to copy default linphonerc file: \(error.localizedDescription)")
}
}
}
static let corePreferences = CorePreferences(config: config)
static var corePreferences = CorePreferences(config: config)
}
// swiftlint:enable line_length

View file

@ -63,7 +63,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Set up notifications
UNUserNotificationCenter.current().delegate = self
return true
}
@ -159,6 +159,9 @@ struct LinphoneApp: App {
private let voipRegistry = PKPushRegistry(queue: coreQueue)
init() {
#if DEBUG
LinphoneApp.applyUITestMDMConfigIfNeeded()
#endif
if !configAvailable {
voipRegistry.delegate = earlyPushDelegate
voipRegistry.desiredPushTypes = [.voIP]
@ -168,6 +171,26 @@ struct LinphoneApp: App {
}
}
#if DEBUG
/// UI-test hook: reads `UITEST_MDM_CONFIG` (JSON) or `UITEST_MDM_CONFIG_CLEAR=1` from
/// the launch environment and writes it to the managed config key before MDMManager runs.
/// Only active in DEBUG builds.
private static func applyUITestMDMConfigIfNeeded() {
let env = ProcessInfo.processInfo.environment
let key = "com.apple.configuration.managed"
if env["UITEST_MDM_CONFIG_CLEAR"] == "1" {
UserDefaults.standard.removeObject(forKey: key)
return
}
guard let json = env["UITEST_MDM_CONFIG"],
let data = json.data(using: .utf8),
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return
}
UserDefaults.standard.set(dict, forKey: key)
}
#endif
var body: some Scene {
WindowGroup {
if configAvailable {

View file

@ -35,7 +35,8 @@ struct SplashScreen: View {
.foregroundColor(ThemeManager.shared.currentTheme.main500)
if showSpinner {
ProgressView()
PopupLoadingView()
.background(.black.opacity(0.65))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

View file

@ -180,6 +180,7 @@ struct PermissionsFragment: View {
)
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
.padding(.horizontal)
.accessibilityIdentifier("permissions_skip_button")
Button {
permissionManager.getPermissions()

View file

@ -370,7 +370,7 @@ struct ContentView: View {
Button(action: {
resetFilter()
sharedMainViewModel.changeIndexView(indexViewInt: 1)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedConversation = nil
@ -395,6 +395,7 @@ struct ContentView: View {
}
})
.padding(.top)
.accessibilityIdentifier("bottom_bar_calls_button")
}
Spacer()
@ -435,7 +436,7 @@ struct ContentView: View {
.resizable()
.foregroundStyle(sharedMainViewModel.indexView == 2 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
if sharedMainViewModel.indexView == 2 {
Text("bottom_navigation_conversations_label")
.default_text_style_700(styleSize: 10)
@ -446,6 +447,7 @@ struct ContentView: View {
}
})
.padding(.top)
.accessibilityIdentifier("bottom_bar_chat_button")
}
Spacer()

View file

@ -55,7 +55,7 @@ struct WelcomeView: View {
Text("welcome_carousel_skip")
.underline()
.default_text_style_600(styleSize: 15)
})
.padding(.top, -35)
.padding(.trailing, 20)
@ -64,6 +64,7 @@ struct WelcomeView: View {
self.index = 2
}
)
.accessibilityIdentifier("welcome_skip_button")
Text("welcome_page_title")
.default_text_style_800(styleSize: 35)
.padding(.trailing, 100)

View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import CryptoKit
import linphonesw
class MDMManager {
static let shared = MDMManager()
static let configurationRemovedNotification = Notification.Name("MDMManager.configurationRemoved")
static let configurationAppliedNotification = Notification.Name("MDMManager.configurationApplied")
private static let hasMDMConfigKey = "MDMManager.hasMDMConfig"
private static let lastXMLConfigSHA256Key = "MDMManager.lastXMLConfigSHA256"
private static let lastCoreConfigSHA256Key = "MDMManager.lastCoreConfigSHA256"
private var hasMDMConfig: Bool {
get { UserDefaults.standard.bool(forKey: MDMManager.hasMDMConfigKey) }
set {
guard UserDefaults.standard.bool(forKey: MDMManager.hasMDMConfigKey) != newValue else { return }
UserDefaults.standard.set(newValue, forKey: MDMManager.hasMDMConfigKey)
}
}
private var lastXMLConfigSHA256: String? {
get { UserDefaults.standard.string(forKey: MDMManager.lastXMLConfigSHA256Key) }
set {
guard UserDefaults.standard.string(forKey: MDMManager.lastXMLConfigSHA256Key) != newValue else { return }
UserDefaults.standard.set(newValue, forKey: MDMManager.lastXMLConfigSHA256Key)
}
}
private var lastCoreConfigSHA256: String? {
get { UserDefaults.standard.string(forKey: MDMManager.lastCoreConfigSHA256Key) }
set {
guard UserDefaults.standard.string(forKey: MDMManager.lastCoreConfigSHA256Key) != newValue else { return }
UserDefaults.standard.set(newValue, forKey: MDMManager.lastCoreConfigSHA256Key)
}
}
private var isApplyingConfig = false
private var lastSeenManagedConfigSignature: String?
func managedConfigChangedSinceLastCheck() -> Bool {
let signature: String
if let mdmConfig = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed") {
signature = sha256Hash(of: mdmConfig)
} else {
signature = ""
}
if signature == lastSeenManagedConfigSignature { return false }
lastSeenManagedConfigSignature = signature
return true
}
func loadXMLConfigFromMdm(config: Config) {
guard let mdmConfig = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed"),
let xmlConfig = mdmConfig["xml-config"] as? String else {
lastXMLConfigSHA256 = nil
return
}
let hash = sha256Hash(of: ["xml-config": xmlConfig])
if hash == lastXMLConfigSHA256 {
Log.info("[MDMManager] xml-config unchanged (SHA256: \(hash)), skipping")
return
}
lastXMLConfigSHA256 = hash
do {
try config.loadFromXmlString(buffer: xmlConfig)
Log.info("[MDMManager] xml-config applied (\(xmlConfig.count) chars)")
} catch let error {
Log.error("[MDMManager] Failed loading xml-config: error = \(error) xml = \(xmlConfig)")
}
}
func applyMdmConfigToCore(core: Core) {
guard !isApplyingConfig else { return }
isApplyingConfig = true
defer {
isApplyingConfig = false
_ = managedConfigChangedSinceLastCheck()
}
loadXMLConfigFromMdm(config: core.config!)
guard let mdmConfig = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed") else {
if hasMDMConfig {
Log.info("[MDMManager] Managed configuration was removed")
lastXMLConfigSHA256 = nil
lastCoreConfigSHA256 = nil
hasMDMConfig = false
handleConfigurationRemoved()
} else {
Log.info("[MDMManager] Managed configuration is empty")
}
return
}
hasMDMConfig = true
let subset: [String: Any] = [
"root-ca": mdmConfig["root-ca"] ?? "",
"config-uri": mdmConfig["config-uri"] ?? ""
]
let hash = sha256Hash(of: subset)
if hash == lastCoreConfigSHA256 {
Log.info("[MDMManager] root-ca/config-uri unchanged (SHA256: \(hash)), skipping")
NotificationCenter.default.post(name: MDMManager.configurationAppliedNotification, object: self, userInfo: ["config": mdmConfig])
return
}
lastCoreConfigSHA256 = hash
let currentProvisioningUri = core.provisioningUri
if let rootCa = mdmConfig["root-ca"] as? String {
core.rootCaData = rootCa
Log.info("[MDMManager] root-ca applied (\(rootCa.count) chars)")
}
if let configUri = mdmConfig["config-uri"] as? String {
do {
if configUri != currentProvisioningUri {
try core.setProvisioninguri(newValue: configUri)
Log.info("[MDMManager] config-uri applied \(configUri)")
}
} catch let error {
Log.error("[MDMManager] Failed setting provisioning URI: error = \(error) configUri = \(configUri)")
}
}
if core.globalState == .On {
do {
core.stop()
try core.start()
} catch let error {
Log.error("[MDMManager] Failed restarting core: error = \(error)")
}
}
NotificationCenter.default.post(name: MDMManager.configurationAppliedNotification, object: self, userInfo: ["config": mdmConfig])
}
private func handleConfigurationRemoved() {
Log.info("[MDMManager] handleConfigurationRemoved - posting notification")
NotificationCenter.default.post(name: MDMManager.configurationRemovedNotification, object: self)
}
private func sha256Hash(of dict: [String: Any]) -> String {
let description = dict.sorted(by: { $0.key < $1.key }).map { "\($0.key)=\($0.value)" }.joined(separator: "&")
let digest = SHA256.hash(data: Data(description.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
}

View file

@ -50,6 +50,7 @@
C642277C2E8E4D900094FEDC /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C642277A2E8E4AC50094FEDC /* ThemeManager.swift */; };
C642277D2E8E4E2B0094FEDC /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071D2AC5922E0037746F /* ColorExtension.swift */; };
C65270F82F879D2700FF248C /* EarlyPushkitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65270F72F879D2700FF248C /* EarlyPushkitDelegate.swift */; };
C65271062F87D3E600FF248C /* MDMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65271052F87D3E600FF248C /* MDMManager.swift */; };
C67586AE2C09F23C002E77BF /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586AD2C09F23C002E77BF /* URLExtension.swift */; };
C67586B02C09F247002E77BF /* URIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586AF2C09F247002E77BF /* URIHandler.swift */; };
C67586B52C09F617002E77BF /* SingleSignOnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586B22C09F617002E77BF /* SingleSignOnManager.swift */; };
@ -240,6 +241,20 @@
remoteGlobalIDString = 660AAF7A2B839271004C0FA6;
remoteInfo = msgNotificationService;
};
C65271122F88D1CA00FF248C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D719ABAB2ABC67BF00B41C10 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D719ABB22ABC67BF00B41C10;
remoteInfo = LinphoneApp;
};
C6E408B32F8E1A33003E0F8C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D719ABAB2ABC67BF00B41C10 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D719ABB22ABC67BF00B41C10;
remoteInfo = LinphoneApp;
};
D7458F372E0BDCF4000C957A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D719ABAB2ABC67BF00B41C10 /* Project object */;
@ -309,6 +324,8 @@
C62817332C1C7C7400DBA646 /* HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpView.swift; sourceTree = "<group>"; };
C642277A2E8E4AC50094FEDC /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
C65270F72F879D2700FF248C /* EarlyPushkitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyPushkitDelegate.swift; sourceTree = "<group>"; };
C65271052F87D3E600FF248C /* MDMManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDMManager.swift; sourceTree = "<group>"; };
C652710C2F88D1CA00FF248C /* LinphoneAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LinphoneAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C67586AD2C09F23C002E77BF /* URLExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = "<group>"; };
C67586AF2C09F247002E77BF /* URIHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URIHandler.swift; sourceTree = "<group>"; };
C67586B22C09F617002E77BF /* SingleSignOnManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleSignOnManager.swift; sourceTree = "<group>"; };
@ -318,6 +335,7 @@
C6A5A9472C10B6A30070FEA4 /* AuthState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthState.swift; sourceTree = "<group>"; };
C6DC4E3C2C199C4E009096FD /* BundleExtenion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtenion.swift; sourceTree = "<group>"; };
C6DC4E3E2C19C289009096FD /* SideMenuEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuEntry.swift; sourceTree = "<group>"; };
C6E408AF2F8E1A33003E0F8C /* LinphoneAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LinphoneAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D703F7072DC8C5FF005B8F75 /* FilePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePicker.swift; sourceTree = "<group>"; };
D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; };
@ -537,6 +555,8 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
C652710D2F88D1CA00FF248C /* LinphoneAppUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LinphoneAppUITests; sourceTree = "<group>"; };
C6E408B02F8E1A33003E0F8C /* LinphoneAppTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LinphoneAppTests; sourceTree = "<group>"; };
D7458F302E0BDCF4000C957A /* linphoneExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D7458F3C2E0BDCF4000C957A /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = linphoneExtension; sourceTree = "<group>"; };
D792F15B2F02BCC2002E3225 /* intentsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D792F1642F02BCC2002E3225 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = intentsExtension; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
@ -552,6 +572,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C65271092F88D1CA00FF248C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C6E408AC2F8E1A33003E0F8C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D719ABB02ABC67BF00B41C10 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -698,6 +732,7 @@
D717071C2AC591EF0037746F /* Utils */ = {
isa = PBXGroup;
children = (
C65271052F87D3E600FF248C /* MDMManager.swift */,
D7BC10D82F4EF64E00F09BDA /* AudioMode.swift */,
D7BC10D32F4EF18100F09BDA /* SoundPlayer.swift */,
C642277A2E8E4AC50094FEDC /* ThemeManager.swift */,
@ -732,6 +767,8 @@
660AAF7C2B839272004C0FA6 /* msgNotificationService */,
D7458F302E0BDCF4000C957A /* linphoneExtension */,
D792F15B2F02BCC2002E3225 /* intentsExtension */,
C652710D2F88D1CA00FF248C /* LinphoneAppUITests */,
C6E408B02F8E1A33003E0F8C /* LinphoneAppTests */,
D7DF8BE52E2104DC003A3BC7 /* Frameworks */,
D719ABB42ABC67BF00B41C10 /* Products */,
D7AEB9462F29128500298546 /* Shared.xcconfig */,
@ -745,6 +782,8 @@
660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */,
D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */,
D792F1582F02BCC2002E3225 /* intentsExtension.appex */,
C652710C2F88D1CA00FF248C /* LinphoneAppUITests.xctest */,
C6E408AF2F8E1A33003E0F8C /* LinphoneAppTests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -1261,6 +1300,52 @@
productReference = 660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */;
productType = "com.apple.product-type.app-extension";
};
C652710B2F88D1CA00FF248C /* LinphoneAppUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = C65271142F88D1CA00FF248C /* Build configuration list for PBXNativeTarget "LinphoneAppUITests" */;
buildPhases = (
C65271082F88D1CA00FF248C /* Sources */,
C65271092F88D1CA00FF248C /* Frameworks */,
C652710A2F88D1CA00FF248C /* Resources */,
);
buildRules = (
);
dependencies = (
C65271132F88D1CA00FF248C /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
C652710D2F88D1CA00FF248C /* LinphoneAppUITests */,
);
name = LinphoneAppUITests;
packageProductDependencies = (
);
productName = LinphoneAppUITests;
productReference = C652710C2F88D1CA00FF248C /* LinphoneAppUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
C6E408AE2F8E1A33003E0F8C /* LinphoneAppTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = C6E408B52F8E1A33003E0F8C /* Build configuration list for PBXNativeTarget "LinphoneAppTests" */;
buildPhases = (
C6E408AB2F8E1A33003E0F8C /* Sources */,
C6E408AC2F8E1A33003E0F8C /* Frameworks */,
C6E408AD2F8E1A33003E0F8C /* Resources */,
);
buildRules = (
);
dependencies = (
C6E408B42F8E1A33003E0F8C /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
C6E408B02F8E1A33003E0F8C /* LinphoneAppTests */,
);
name = LinphoneAppTests;
packageProductDependencies = (
);
productName = LinphoneAppTests;
productReference = C6E408AF2F8E1A33003E0F8C /* LinphoneAppTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
D719ABB22ABC67BF00B41C10 /* LinphoneApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = D719ABC22ABC67BF00B41C10 /* Build configuration list for PBXNativeTarget "LinphoneApp" */;
@ -1342,6 +1427,14 @@
CreatedOnToolsVersion = 15.0.1;
LastSwiftMigration = 1500;
};
C652710B2F88D1CA00FF248C = {
CreatedOnToolsVersion = 26.2;
TestTargetID = D719ABB22ABC67BF00B41C10;
};
C6E408AE2F8E1A33003E0F8C = {
CreatedOnToolsVersion = 26.2;
TestTargetID = D719ABB22ABC67BF00B41C10;
};
D719ABB22ABC67BF00B41C10 = {
CreatedOnToolsVersion = 14.3.1;
};
@ -1392,6 +1485,8 @@
660AAF7A2B839271004C0FA6 /* msgNotificationService */,
D7458F2E2E0BDCF4000C957A /* linphoneExtension */,
D792F1572F02BCC2002E3225 /* intentsExtension */,
C652710B2F88D1CA00FF248C /* LinphoneAppUITests */,
C6E408AE2F8E1A33003E0F8C /* LinphoneAppTests */,
);
};
/* End PBXProject section */
@ -1407,6 +1502,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C652710A2F88D1CA00FF248C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C6E408AD2F8E1A33003E0F8C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D719ABB12ABC67BF00B41C10 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -1500,6 +1609,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C65271082F88D1CA00FF248C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C6E408AB2F8E1A33003E0F8C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D719ABAF2ABC67BF00B41C10 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -1571,6 +1694,7 @@
D7E2E69F2CE356C90080DA0D /* PopupViewWithTextField.swift in Sources */,
C67586B52C09F617002E77BF /* SingleSignOnManager.swift in Sources */,
D7F4D9CB2B5FD27200CDCD76 /* CallsListFragment.swift in Sources */,
C65271062F87D3E600FF248C /* MDMManager.swift in Sources */,
C6A5A9482C10B6A30070FEA4 /* AuthState.swift in Sources */,
D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */,
D711B1342E93F18700DF8C71 /* LdapServerConfigurationFragment.swift in Sources */,
@ -1712,6 +1836,16 @@
target = 660AAF7A2B839271004C0FA6 /* msgNotificationService */;
targetProxy = 660AAF7D2B839272004C0FA6 /* PBXContainerItemProxy */;
};
C65271132F88D1CA00FF248C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D719ABB22ABC67BF00B41C10 /* LinphoneApp */;
targetProxy = C65271122F88D1CA00FF248C /* PBXContainerItemProxy */;
};
C6E408B42F8E1A33003E0F8C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D719ABB22ABC67BF00B41C10 /* LinphoneApp */;
targetProxy = C6E408B32F8E1A33003E0F8C /* PBXContainerItemProxy */;
};
D7458F382E0BDCF4000C957A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D7458F2E2E0BDCF4000C957A /* linphoneExtension */;
@ -1863,6 +1997,120 @@
};
name = Release;
};
C65271152F88D1CA00FF248C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = LinphoneApp;
};
name = Debug;
};
C65271162F88D1CA00FF248C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = LinphoneApp;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
C6E408B62F8E1A33003E0F8C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LinphoneApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LinphoneApp";
};
name = Debug;
};
C6E408B72F8E1A33003E0F8C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LinphoneApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LinphoneApp";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
D719ABC02ABC67BF00B41C10 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D7AEB9462F29128500298546 /* Shared.xcconfig */;
@ -2260,6 +2508,24 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C65271142F88D1CA00FF248C /* Build configuration list for PBXNativeTarget "LinphoneAppUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C65271152F88D1CA00FF248C /* Debug */,
C65271162F88D1CA00FF248C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C6E408B52F8E1A33003E0F8C /* Build configuration list for PBXNativeTarget "LinphoneAppTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C6E408B62F8E1A33003E0F8C /* Debug */,
C6E408B72F8E1A33003E0F8C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D719ABAE2ABC67BF00B41C10 /* Build configuration list for PBXProject "LinphoneApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View file

@ -28,6 +28,30 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C652710B2F88D1CA00FF248C"
BuildableName = "LinphoneAppUITests.xctest"
BlueprintName = "LinphoneAppUITests"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E408AE2F8E1A33003E0F8C"
BuildableName = "LinphoneAppTests.xctest"
BlueprintName = "LinphoneAppTests"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E408AE2F8E1A33003E0F8C"
BuildableName = "LinphoneAppTests.xctest"
BlueprintName = "LinphoneAppTests"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C652710B2F88D1CA00FF248C"
BuildableName = "LinphoneAppUITests.xctest"
BlueprintName = "LinphoneAppUITests"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D792F1572F02BCC2002E3225"
BuildableName = "intentsExtension.appex"
BlueprintName = "intentsExtension"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E408AE2F8E1A33003E0F8C"
BuildableName = "LinphoneAppTests.xctest"
BlueprintName = "LinphoneAppTests"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D7458F2E2E0BDCF4000C957A"
BuildableName = "linphoneExtension.appex"
BlueprintName = "linphoneExtension"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E408AE2F8E1A33003E0F8C"
BuildableName = "LinphoneAppTests.xctest"
BlueprintName = "LinphoneAppTests"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "660AAF7A2B839271004C0FA6"
BuildableName = "msgNotificationService.appex"
BlueprintName = "msgNotificationService"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E408AE2F8E1A33003E0F8C"
BuildableName = "LinphoneAppTests.xctest"
BlueprintName = "LinphoneAppTests"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D719ABB22ABC67BF00B41C10"
BuildableName = "LinphoneApp.app"
BlueprintName = "LinphoneApp"
ReferencedContainer = "container:LinphoneApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import XCTest
import linphonesw
@testable import LinphoneApp
class MDMManagerTests: XCTestCase {
private let managedKey = "com.apple.configuration.managed"
private let dummyRootCa = """
-----BEGIN CERTIFICATE-----
MIIDdummyTESTCERTIFICATEVALUEForUnitTestsOnly1234567890abcdefABCDEF
-----END CERTIFICATE-----
"""
override func setUp() {
super.setUp()
UserDefaults.standard.removeObject(forKey: managedKey)
UserDefaults.standard.removeObject(forKey: "MDMManager.hasMDMConfig")
UserDefaults.standard.removeObject(forKey: "MDMManager.lastConfigSHA256")
}
override func tearDown() {
UserDefaults.standard.removeObject(forKey: managedKey)
UserDefaults.standard.removeObject(forKey: "MDMManager.hasMDMConfig")
UserDefaults.standard.removeObject(forKey: "MDMManager.lastConfigSHA256")
super.tearDown()
}
func testApplyMdmConfigSetsRootCa() throws {
let mdmConfig: [String: Any] = ["root-ca": dummyRootCa]
UserDefaults.standard.set(mdmConfig, forKey: managedKey)
let config = Config.newForSharedCore(
appGroupId: Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_NAME") as? String ?? "group.test",
configFilename: "linphonerc-test",
factoryConfigFilename: nil
)
let core = try Factory.Instance.createCoreWithConfig(config: config!, systemContext: nil)
let appliedExpectation = expectation(forNotification: MDMManager.configurationAppliedNotification, object: nil) { notification in
guard let config = notification.userInfo?["config"] as? [String: Any] else { return false }
return (config["root-ca"] as? String) == self.dummyRootCa
}
MDMManager.shared.applyMdmConfigToCore(core: core)
wait(for: [appliedExpectation], timeout: 5)
XCTAssertEqual(core.rootCaData, dummyRootCa,
"core.rootCaData should equal the MDM-provided root-ca after applyMdmConfigToCore")
}
}

View file

@ -0,0 +1,169 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import XCTest
class MDMChatFeatureUITests: XCTestCase {
let app = XCUIApplication()
private var permissionMonitor: NSObjectProtocol?
override func setUp() {
super.setUp()
continueAfterFailure = false
permissionMonitor = addUIInterruptionMonitor(withDescription: "System Permission Alert") { alert in
let prefixMatches = ["Share All", "Allow"]
for prefix in prefixMatches {
let match = alert.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", prefix)).firstMatch
if match.exists {
match.tap()
return true
}
}
let allowLabels = ["Continue", "OK", "Allow Once", "Always Allow"]
for label in allowLabels {
let button = alert.buttons[label]
if button.exists {
button.tap()
return true
}
}
return false
}
}
override func tearDown() {
if let monitor = permissionMonitor {
removeUIInterruptionMonitor(monitor)
}
super.tearDown()
}
private func requiredEnv(_ name: String) -> String {
guard let value = ProcessInfo.processInfo.environment[name], !value.isEmpty else {
XCTFail("Missing required env var \(name). Export TEST_RUNNER_\(name) before running scripts/run-mdm-tests.sh")
return ""
}
return value
}
private func launchWithMDM(_ mdm: [String: Any]) {
let data = try! JSONSerialization.data(withJSONObject: mdm)
app.launchEnvironment["UITEST_MDM_CONFIG"] = String(data: data, encoding: .utf8)!
app.launch()
_ = app.wait(for: .runningForeground, timeout: 15)
}
private func skipOnboardingIfShown() {
let welcomeSkip = app.buttons["welcome_skip_button"]
if welcomeSkip.waitForExistence(timeout: 5) {
welcomeSkip.tap()
}
let permissionsSkip = app.buttons["permissions_skip_button"]
if permissionsSkip.waitForExistence(timeout: 5) {
permissionsSkip.tap()
}
}
private func waitForMainPage(timeout: TimeInterval) -> Bool {
let callsButton = app.buttons.matching(NSPredicate(format: "label == %@", "Calls")).firstMatch
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let alertPrefixes = ["Share All", "Allow", "Continue", "OK"]
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
for prefix in alertPrefixes {
let button = springboard.buttons.matching(
NSPredicate(format: "label BEGINSWITH %@", prefix)
).firstMatch
if button.exists {
button.tap()
break
}
}
if callsButton.exists {
return true
}
_ = callsButton.waitForExistence(timeout: 1)
}
return false
}
private func dumpOnFailure(_ label: String) {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
print("=== APP DEBUG DESCRIPTION (\(label)) ===")
print(app.debugDescription)
print("=== SPRINGBOARD DEBUG DESCRIPTION =====================")
print(springboard.debugDescription)
print("=======================================================")
}
func testChatButtonHiddenWithMDMDisableChat() {
let username = requiredEnv("LINPHONE_TEST_USERNAME")
let ha1 = requiredEnv("LINPHONE_TEST_HA1")
let domain = ProcessInfo.processInfo.environment["LINPHONE_TEST_DOMAIN"] ?? "sip.linphone.org"
let xmlConfig = """
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd">
<section name="ui">
<entry name="disable_chat_feature">1</entry>
</section>
<section name="proxy_0">
<entry name="reg_identity" overwrite="true">sip:\(username)@\(domain)</entry>
<entry name="reg_proxy" overwrite="true">&lt;sip:\(domain);transport=tls&gt;</entry>
<entry name="reg_route" overwrite="true">&lt;sip:\(domain);transport=tls&gt;</entry>
<entry name="realm" overwrite="true">\(domain)</entry>
<entry name="lime_server_url" overwrite="true">https://lime.linphone.org:443/lime-server/lime-server.php</entry>
</section>
<section name="auth_info_0">
<entry name="username" overwrite="true">\(username)</entry>
<entry name="domain" overwrite="true">\(domain)</entry>
<entry name="ha1" overwrite="true">\(ha1)</entry>
<entry name="realm" overwrite="true">\(domain)</entry>
<entry name="algorithm" overwrite="true">MD5</entry>
</section>
</config>
"""
launchWithMDM(["xml-config": xmlConfig])
skipOnboardingIfShown()
let landed = waitForMainPage(timeout: 30)
if !landed { dumpOnFailure("main screen not reached") }
XCTAssertTrue(landed, "Should have landed on the main screen after Welcome + Permissions")
let chatButton = app.buttons.matching(NSPredicate(format: "label == %@", "Conversations")).firstMatch
XCTAssertFalse(chatButton.exists,
"Chat button should NOT be visible when MDM disables chat feature")
}
func testConfigUriMDMLandsOnMainPage() {
let configUri = requiredEnv("LINPHONE_TEST_CONFIG_URI")
launchWithMDM(["config-uri": configUri])
skipOnboardingIfShown()
let landed = waitForMainPage(timeout: 60)
if !landed { dumpOnFailure("main screen not reached with config-uri \(configUri)") }
XCTAssertTrue(landed,
"Should have landed on the main screen after remote provisioning from \(configUri)")
}
}

114
README.md
View file

@ -133,3 +133,117 @@ cmake --preset=ios-sdk -G Ninja -B spm-ios && cmake --build spm-ios
- Add it manually if needed.
![Image6](ReadmeImages/ReadmeImage6.png)
# MDM (Mobile Device Management) configuration
Linphone iOS supports managed app configuration via the standard iOS MDM
`com.apple.configuration.managed` mechanism. When the app is deployed through
an MDM server, administrators can push a configuration dictionary that the app
reads at startup and whenever the managed configuration changes at runtime.
The following keys are supported:
| Key | Type | Description |
|---------------|--------|-------------------------------------------------------------------------------------------------|
| `xml-config` | String | A Linphone configuration in XML format (same schema as `linphonerc`). Applied via `Config.loadFromXmlString`. |
| `root-ca` | String | A PEM-encoded root CA certificate used by the Linphone SDK for TLS operations (SIPS, HTTPS provisioning, …). Applied to `core.rootCaData`. |
| `config-uri` | String | URI to a remote provisioning file. When set, it takes precedence over any `config-uri` that may be defined inside `xml-config`, and triggers a core restart to fetch the remote configuration. |
Notes:
- All three keys are optional and can be combined.
- If `config-uri` is present, it is set last and the core is restarted so that
remote provisioning takes effect; any `config-uri` value embedded in
`xml-config` is therefore overridden.
- Applying and removing the managed configuration at runtime is supported:
removing it resets the core to its default configuration and returns to the
assistant/login screen.
## Testing MDM configuration
Two kinds of tests are provided:
### UI tests (end-to-end)
Located in `LinphoneAppUITests/MDMChatFeatureUITests.swift`. They inject a
managed configuration at launch via the app's DEBUG-only
`UITEST_MDM_CONFIG` launch-environment hook (implemented in
`Linphone/LinphoneApp.swift`), so no `xcrun simctl` setup is needed.
Note: the tests only cover MDM *application* (fresh launch with a managed
config). Live removal of MDM while the app is running cannot be simulated
from an XCUITest process (UserDefaults is per-process and we want the app
to stay alive for a realistic removal scenario), so that path is covered by
manual testing only.
Each MDM test case represents "a fresh device receiving a specific managed
configuration", so we uninstall the app before every test to avoid any
leakage of UserDefaults / keychain / provisioning / accounts between cases.
The wrapper script `scripts/run-mdm-tests.sh` does this for you.
The tests need a real SIP account to reach the main screen (the MDM XML
embeds proxy + auth_info sections) and a remote provisioning URL for the
config-uri test. Credentials can be provided three ways — the script
resolves them in this order, highest first:
1. CLI flags: `--username`, `--ha1`, `--domain`, `--config-uri` (and
`--device` for the sim UUID)
2. Shell env vars: `LINPHONE_TEST_USERNAME`, `LINPHONE_TEST_HA1`,
`LINPHONE_TEST_DOMAIN`, `LINPHONE_TEST_CONFIG_URI`
3. The gitignored file `scripts/test-credentials.env` (copy from `.env.example`)
Examples:
```bash
scripts/run-mdm-tests.sh --device <uuid> --username alice --ha1 <md5-hash> --config-uri https://example.com/provisioning.xml
```
```bash
cp scripts/test-credentials.env.example scripts/test-credentials.env
# edit scripts/test-credentials.env, fill in LINPHONE_TEST_USERNAME /
# LINPHONE_TEST_HA1 / LINPHONE_TEST_CONFIG_URI
scripts/run-mdm-tests.sh
```
It will create+boot a throwaway simulator if `DEVICE_UUID` is not set,
uninstall the app before each test, run the tests one at a time with
`-parallel-testing-enabled NO`, and clean up at the end. To reuse an
already-booted simulator:
```bash
DEVICE_UUID=<your-booted-simulator-uuid> scripts/run-mdm-tests.sh
```
`-parallel-testing-enabled NO` avoids flaky UI test launch failures caused by
Xcode spinning up multiple simulator clones in parallel (the test-runner app
can fail to launch on a clone under pressure).
Covered cases:
- `testChatButtonHiddenWithMDMDisableChat` — MDM `xml-config` with
`disable_chat_feature=1`; the test reaches the main screen and asserts the
chat button is hidden.
- `testConfigUriMDMLandsOnMainPage` — MDM `config-uri` pointing at the URL
supplied via `--config-uri` / `LINPHONE_TEST_CONFIG_URI`; the test verifies
that remote provisioning completes and the app lands on the main screen.
### Unit tests (MDMManager)
Located in `LinphoneAppTests/MDMManagerTests.swift`. The unit test covers
only `root-ca` application: it calls
`MDMManager.shared.applyMdmConfigToCore(core:)` directly on a throwaway
`Core` and asserts `core.rootCaData` matches the MDM-provided certificate.
The `config-uri` and `xml-config` paths are exercised end-to-end by the UI
tests above.
This requires a **Unit Testing Bundle** target in Xcode (separate from the UI
test target, because `@testable import Linphone` only works from a unit test
bundle):
1. Xcode → File → New → Target → iOS → Unit Testing Bundle
2. Name it `LinphoneAppTests`, set "Target to be Tested" to `LinphoneApp`
3. Add `LinphoneAppTests/MDMManagerTests.swift` to that target
Then run:
```bash
xcodebuild test -project LinphoneApp.xcodeproj -scheme LinphoneAppTests -destination "platform=iOS Simulator,id=$DEVICE_UUID"
```

155
scripts/run-mdm-tests.sh Executable file
View file

@ -0,0 +1,155 @@
#!/usr/bin/env bash
#
# Run MDM UI tests with a clean app install before each test case.
#
# Each MDM UI test scenario represents a fresh device receiving a specific
# managed configuration. To reproduce that "closest to reality", we uninstall
# the app before every single test so no state (UserDefaults, keychain,
# provisioning, accounts) leaks between runs.
#
# Usage:
# scripts/run-mdm-tests.sh [--device <uuid>] [--username <user>] [--ha1 <hash>] [--domain <domain>]
#
# Or with environment variables / the gitignored `scripts/test-credentials.env`:
# DEVICE_UUID=<uuid> LINPHONE_TEST_USERNAME=... LINPHONE_TEST_HA1=... scripts/run-mdm-tests.sh
#
# Resolution order (highest first): CLI flag > shell env > test-credentials.env > default.
#
# If no device UUID is given, the script creates and boots a throwaway
# "iPhone 15" simulator, then deletes it at the end.
#
# SIP test credentials are required for tests that need a working account.
# They are forwarded to the UI test runner via the `TEST_RUNNER_` prefix,
# which Xcode strips before exposing them to the test process.
# LINPHONE_TEST_USERNAME SIP username (required)
# LINPHONE_TEST_HA1 md5(username:realm:password) (required)
# LINPHONE_TEST_DOMAIN defaults to sip.linphone.org
# LINPHONE_TEST_CONFIG_URI remote provisioning URL (required for the
# config-uri UI test)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/test-credentials.env" ]]; then
# shellcheck source=/dev/null
source "$SCRIPT_DIR/test-credentials.env"
fi
CLI_DEVICE=""
CLI_USERNAME=""
CLI_HA1=""
CLI_DOMAIN=""
CLI_CONFIG_URI=""
while [[ $# -gt 0 ]]; do
case "$1" in
--device) CLI_DEVICE="$2"; shift 2 ;;
--username) CLI_USERNAME="$2"; shift 2 ;;
--ha1) CLI_HA1="$2"; shift 2 ;;
--domain) CLI_DOMAIN="$2"; shift 2 ;;
--config-uri) CLI_CONFIG_URI="$2"; shift 2 ;;
-h|--help)
sed -n '3,20p' "$0"
exit 0
;;
-*)
echo "Unknown flag: $1" >&2
exit 2
;;
*)
# First bare positional arg = device UUID (back-compat).
if [[ -z "$CLI_DEVICE" ]]; then CLI_DEVICE="$1"; shift
else echo "Unexpected argument: $1" >&2; exit 2
fi
;;
esac
done
LINPHONE_TEST_USERNAME="${CLI_USERNAME:-${LINPHONE_TEST_USERNAME:-}}"
LINPHONE_TEST_HA1="${CLI_HA1:-${LINPHONE_TEST_HA1:-}}"
LINPHONE_TEST_DOMAIN="${CLI_DOMAIN:-${LINPHONE_TEST_DOMAIN:-sip.linphone.org}}"
LINPHONE_TEST_CONFIG_URI="${CLI_CONFIG_URI:-${LINPHONE_TEST_CONFIG_URI:-}}"
: "${LINPHONE_TEST_USERNAME:?LINPHONE_TEST_USERNAME is required (pass --username, export it, or put it in scripts/test-credentials.env)}"
: "${LINPHONE_TEST_HA1:?LINPHONE_TEST_HA1 is required (pass --ha1, export it, or put it in scripts/test-credentials.env)}"
: "${LINPHONE_TEST_CONFIG_URI:?LINPHONE_TEST_CONFIG_URI is required (pass --config-uri, export it, or put it in scripts/test-credentials.env)}"
export TEST_RUNNER_LINPHONE_TEST_USERNAME="$LINPHONE_TEST_USERNAME"
export TEST_RUNNER_LINPHONE_TEST_HA1="$LINPHONE_TEST_HA1"
export TEST_RUNNER_LINPHONE_TEST_DOMAIN="$LINPHONE_TEST_DOMAIN"
export TEST_RUNNER_LINPHONE_TEST_CONFIG_URI="$LINPHONE_TEST_CONFIG_URI"
BUNDLE_ID="org.linphone.phone"
PROJECT="LinphoneApp.xcodeproj"
SCHEME="LinphoneAppUITests"
TEST_CLASS="LinphoneAppUITests/MDMChatFeatureUITests"
# App groups survive `simctl uninstall`, so linphonerc / SDK state from a
# previous test leaks into the next fresh install. We nuke these between
# tests so each test really starts from scratch.
APP_GROUPS=(
"group.org.linphone.phone.msgNotification"
"group.org.linphone.phone.linphoneExtension"
)
TESTS=(
"testChatButtonHiddenWithMDMDisableChat"
"testConfigUriMDMLandsOnMainPage"
)
CREATED_DEVICE=0
DEVICE_UUID="${CLI_DEVICE:-${DEVICE_UUID:-}}"
if [[ -z "$DEVICE_UUID" ]]; then
echo "No DEVICE_UUID provided, creating a throwaway simulator..."
DEVICE_UUID=$(xcrun simctl create "LinphoneMDMTest" "iPhone 15")
xcrun simctl boot "$DEVICE_UUID"
CREATED_DEVICE=1
else
# Make sure the simulator is booted — simctl uninstall fails on Shutdown.
# `simctl boot` is a no-op on an already-booted device except for exit code,
# so we ignore that specific error.
xcrun simctl boot "$DEVICE_UUID" 2>/dev/null || true
fi
cleanup() {
if [[ "$CREATED_DEVICE" == "1" ]]; then
xcrun simctl shutdown "$DEVICE_UUID" || true
xcrun simctl delete "$DEVICE_UUID" || true
fi
}
trap cleanup EXIT
for test in "${TESTS[@]}"; do
echo ""
echo "=============================================="
echo "Running $test on $DEVICE_UUID"
echo "=============================================="
# Wipe app group containers BEFORE uninstall — get_app_container needs the
# app installed to resolve the group path. On the very first iteration the
# app isn't installed yet (nothing to leak), so the lookup silently no-ops.
for group in "${APP_GROUPS[@]}"; do
group_path=$(xcrun simctl get_app_container "$DEVICE_UUID" "$BUNDLE_ID" "$group" 2>/dev/null || true)
if [[ -n "$group_path" && -d "$group_path" ]]; then
echo "Wiping app group container: $group_path"
rm -rf "$group_path"/* "$group_path"/.[!.]* 2>/dev/null || true
fi
done
xcrun simctl uninstall "$DEVICE_UUID" "$BUNDLE_ID" || true
# Keychain survives simctl uninstall — once the core materializes an account
# from MDM it saves auth info to the keychain, which would make the next
# fresh install skip the welcome flow. Reset the keychain between tests.
xcrun simctl keychain "$DEVICE_UUID" reset || true
# Pre-grant privacy-sensitive permissions so no system dialogs interrupt the
# flow (Contacts triggers an extra "Share All N Contacts" limited-access
# sheet on iOS 18 that UIInterruptionMonitor can only dismiss if another app
# interaction follows — not worth juggling). Must happen after uninstall +
# before the test launches the app. Ignore failures (not all sims/iOS
# versions support every service).
for service in contacts notifications location location-always camera microphone photos; do
xcrun simctl privacy "$DEVICE_UUID" grant "$service" "$BUNDLE_ID" 2>/dev/null || true
done
xcodebuild test -project "$PROJECT" -scheme "$SCHEME" -destination "platform=iOS Simulator,id=$DEVICE_UUID" -only-testing:"$TEST_CLASS/$test" -parallel-testing-enabled NO
done
echo ""
echo "All MDM UI tests passed."