mirror of
https://github.com/jayanezj/traccar-client-macos.git
synced 2026-11-05 03:00:46 +01:00
feat: persist local configuration and device identity (#3)
Store receiver and identity together in Keychain, with versioned tracking preferences, explicit authorization retry and stable Settings focus. Adopt free Apple Development locally and credential-free ad hoc CI. Run configuration and focus tests through both CI toolchains. Final decisions: https://github.com/jayanezj/traccar-client-macos/issues/3#issuecomment-6396087703
This commit is contained in:
23 files changed
+1442
-42
No files matched your search
@@ -0,0 +1,106 @@
|
||||
import Foundation
|
||||
import Security
|
||||
import Testing
|
||||
|
||||
extension ConfigurationTests {
|
||||
@Test func keychainIsNotReadUntilExplicitLoadAndCancellationDoesNotReplaceIdentity() throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
let existing = ReceiverConnection(serverURL: "https://tracker.example", deviceIdentifier: "000123-test")
|
||||
fixture.connection.value = existing
|
||||
fixture.connection.readError = .keychain(errSecUserCanceled)
|
||||
var generated = 0
|
||||
let model = ConfigurationModel(storage: fixture.storage, generateIdentifier: {
|
||||
generated += 1
|
||||
return "12345678"
|
||||
})
|
||||
#expect(fixture.connection.reads == 0 && !model.hasLoaded)
|
||||
model.reload()
|
||||
#expect(!model.hasLoaded)
|
||||
#expect(model.errorMessage?.contains("cancelled") == true)
|
||||
#expect(model.errorMessage?.contains("not saved") == false)
|
||||
#expect(model.statusMessage == nil)
|
||||
#expect(fixture.connection.reads == 1 && fixture.connection.writes == 0)
|
||||
#expect(fixture.connection.value == existing && generated == 0)
|
||||
#expect(!model.save())
|
||||
#expect(fixture.connection.reads == 1 && fixture.connection.writes == 0)
|
||||
fixture.connection.readError = nil
|
||||
model.reload()
|
||||
#expect(model.hasLoaded && model.errorMessage == nil)
|
||||
#expect(model.draft.connection == existing && generated == 0)
|
||||
#expect(fixture.connection.reads == 2 && fixture.connection.writes == 0)
|
||||
}
|
||||
|
||||
@Test func cancelledWritePreservesLoadedConfigurationAndCanBeRetried() throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
let existing = ReceiverConnection(deviceIdentifier: "000123-test")
|
||||
fixture.connection.value = existing
|
||||
let model = ConfigurationModel(storage: fixture.storage)
|
||||
model.reload()
|
||||
let original = model.saved
|
||||
model.draft.connection.serverURL = "https://new.example"
|
||||
fixture.connection.writeError = .keychain(errSecUserCanceled)
|
||||
#expect(!model.save())
|
||||
#expect(model.errorMessage?.contains("cancelled") == true)
|
||||
#expect(model.hasLoaded && model.saved == original)
|
||||
#expect(fixture.connection.value == existing)
|
||||
#expect(fixture.preferences.object(forKey: PreferencesConfigurationStorage.preferencesKey) == nil)
|
||||
fixture.connection.writeError = nil
|
||||
#expect(model.save())
|
||||
#expect(model.errorMessage == nil)
|
||||
#expect(try fixture.storage.load().connection.serverURL == "https://new.example")
|
||||
#expect(try fixture.storage.load().connection.deviceIdentifier == "000123-test")
|
||||
}
|
||||
}
|
||||
|
||||
extension ConfigurationTests {
|
||||
@Test func trustedAndEmptyStoresLoadAutomaticallyWithoutAuthorizationStep() throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
let initial = ConfigurationModel(storage: fixture.storage)
|
||||
initial.loadIfNeeded()
|
||||
#expect(initial.hasLoaded && !initial.needsKeychainAuthorization)
|
||||
#expect(initial.draft.connection.deviceIdentifier.isEmpty)
|
||||
#expect(fixture.connection.readModes == [false])
|
||||
fixture.connection.value = ReceiverConnection(deviceIdentifier: "000123-test")
|
||||
let reopened = ConfigurationModel(storage: fixture.storage)
|
||||
reopened.loadIfNeeded()
|
||||
reopened.loadIfNeeded()
|
||||
#expect(reopened.hasLoaded && !reopened.needsKeychainAuthorization)
|
||||
#expect(reopened.draft.connection.deviceIdentifier == "000123-test")
|
||||
#expect(fixture.connection.readModes == [false, false])
|
||||
}
|
||||
|
||||
@Test func authorizationRequiredExplainsBeforeExplicitRetryWithoutAutomaticLoop() throws {
|
||||
let fixture = try Fixture()
|
||||
defer { fixture.cleanUp() }
|
||||
fixture.connection.readError = .keychain(errSecInteractionNotAllowed)
|
||||
let model = ConfigurationModel(storage: fixture.storage)
|
||||
model.loadIfNeeded()
|
||||
#expect(!model.hasLoaded && model.needsKeychainAuthorization)
|
||||
#expect(model.errorMessage == nil)
|
||||
model.loadIfNeeded()
|
||||
#expect(fixture.connection.readModes == [false])
|
||||
fixture.connection.readError = .keychain(errSecUserCanceled)
|
||||
model.reload()
|
||||
model.loadIfNeeded()
|
||||
#expect(model.errorMessage?.contains("cancelled") == true)
|
||||
#expect(fixture.connection.readModes == [false, true])
|
||||
fixture.connection.readError = nil
|
||||
model.reload()
|
||||
#expect(model.hasLoaded && !model.needsKeychainAuthorization && model.errorMessage == nil)
|
||||
#expect(fixture.connection.readModes == [false, true, true])
|
||||
}
|
||||
|
||||
@Test func keychainBackendReceivesNoninteractiveReadMode() throws {
|
||||
let operations = FakeKeychainOperations()
|
||||
let storage = KeychainConnectionStorage(service: "test.synthetic", operations: operations)
|
||||
#expect(try storage.read(allowInteraction: false) == nil)
|
||||
operations.readStatus = errSecInteractionNotAllowed
|
||||
#expect(throws: ConfigurationError.keychain(errSecInteractionNotAllowed)) {
|
||||
try storage.read(allowInteraction: false)
|
||||
}
|
||||
#expect(operations.readModes == [false, false])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user