Skip to content

Commit

Permalink
Fully implement measurement tests.
Browse files Browse the repository at this point in the history
* Added new Invitation Code Module and support to automatically inject test account.
* Resolved some Todos
* Fully implement weight scale UI tests.
  • Loading branch information
Supereg committed Jun 3, 2024
1 parent 94aa8b8 commit dc6d9cb
Show file tree
Hide file tree
Showing 16 changed files with 244 additions and 132 deletions.
8 changes: 6 additions & 2 deletions ENGAGEHF.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 97D73D692AD860AD00B47FA0 /* SpeziFirebaseStorage */; };
A92E4DF02BAA001100AC8DE8 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E4DEF2BAA001100AC8DE8 /* OrderedCollections */; };
A96C56B62C0A149B00D6A50B /* WeightScaleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C56B52C0A149B00D6A50B /* WeightScaleTests.swift */; };
A96C56BB2C0DFFCE00D6A50B /* InvitationCodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */; };
A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */; };
A9D83F962B083794000D0C78 /* SpeziFirebaseAccountStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A9D83F952B083794000D0C78 /* SpeziFirebaseAccountStorage */; };
A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; };
Expand Down Expand Up @@ -140,6 +141,7 @@
653A256728338800005D4D48 /* ENGAGEHFUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ENGAGEHFUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
653A258928339462005D4D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A96C56B52C0A149B00D6A50B /* WeightScaleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeightScaleTests.swift; sourceTree = "<group>"; };
A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitationCodeModule.swift; sourceTree = "<group>"; };
A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = "<group>"; };
A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = "<group>"; };
A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -375,6 +377,7 @@
A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */,
A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */,
A9DFE8A82ABE551400428242 /* AccountButton.swift */,
A96C56BA2C0DFFCE00D6A50B /* InvitationCodeModule.swift */,
);
path = Account;
sourceTree = "<group>";
Expand Down Expand Up @@ -583,6 +586,7 @@
2FF53D8D2A8729D600042B76 /* ENGAGEHFStandard.swift in Sources */,
2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */,
4D052DB82BE07892006A784E /* MeasurementManager.swift in Sources */,
A96C56BB2C0DFFCE00D6A50B /* InvitationCodeModule.swift in Sources */,
4DB025D82BBF2EEC002D2545 /* Greeting.swift in Sources */,
A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */,
2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */,
Expand Down Expand Up @@ -1214,8 +1218,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.0;
branch = "fix/weight-scale-feature";
kind = branch;
};
};
5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */ = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziBluetooth.git",
"state" : {
"revision" : "8e94fc71720ef3fcf7f5d9dba9eef603c5151d7a",
"version" : "1.3.0"
"branch" : "fix/weight-scale-feature",
"revision" : "00df06c021749a1b16a3bc383dd62a1d19922bc6"
}
},
{
Expand Down
12 changes: 10 additions & 2 deletions ENGAGEHF.xcodeproj/xcshareddata/xcschemes/ENGAGEHF.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,21 @@
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--showOnboarding"
argument = "--setupTestEnvironment"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--skipOnboarding"
argument = "--testMockDevices"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--showOnboarding"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--skipOnboarding"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--testSchedule"
isEnabled = "NO">
Expand Down
123 changes: 123 additions & 0 deletions ENGAGEHF/Account/InvitationCodeModule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2024 Stanford University
//
// SPDX-License-Identifier: MIT
//

import Firebase
import FirebaseAuth
import FirebaseFunctions
import Spezi
import SpeziAccount
import SpeziFirebaseConfiguration


class InvitationCodeModule: Module, EnvironmentAccessible {
@Dependency private var firebase: ConfigureFirebaseApp

@Application(\.logger) private var logger

func configure() {
if FeatureFlags.useFirebaseEmulator {
Functions.functions().useEmulator(withHost: "localhost", port: 5001)
}
}

func signOutAccount() {
do {
try Auth.auth().signOut()
} catch {
logger.debug("Failed to sing out firebase account: \(error)")
}
}

func verifyOnboardingCode(_ invitationCode: String) async throws {
do {
if FeatureFlags.disableFirebase {
guard invitationCode == "VASCTRAC" else {
throw InvitationCodeError.invitationCodeInvalid
}

try? await Task.sleep(for: .seconds(0.25))
} else {
try Auth.auth().signOut()

async let authResult = Auth.auth().signInAnonymously()
let checkInvitationCode = Functions.functions().httpsCallable("checkInvitationCode")

do {
_ = try await checkInvitationCode.call(
[
"invitationCode": invitationCode,
"userId": authResult.user.uid
]
)
} catch {
logger.error("Failed to check invitation code: \(error)")
throw InvitationCodeError.invitationCodeInvalid
}
}
} catch let error as NSError {
if let errorCode = FunctionsErrorCode(rawValue: error.code) {
// Handle Firebase-specific errors.
switch errorCode {
case .unauthenticated:
throw InvitationCodeError.userNotAuthenticated
case .notFound:
throw InvitationCodeError.invitationCodeInvalid
default:
throw InvitationCodeError.generalError(error.localizedDescription)
}
} else {
// Handle other errors, such as network issues or unexpected behavior.
throw InvitationCodeError.generalError(error.localizedDescription)
}
}
}

func setupTestEnvironment(account: Account, invitationCode: String) async throws {
try await verifyOnboardingCode(invitationCode)
try await setupTestAccount(account: account)
}

@MainActor
private func setupTestAccount(account: Account) async throws {
let email = "[email protected]"
let password = "123456789"

if let details = account.details,
details.email == email {
return
}

guard let service = account.registeredAccountServices.compactMap({ $0 as? any UserIdPasswordAccountService }).first else {
preconditionFailure("Failed to retrieve a user-id-password based account service for test account setup!")
}


do {
// let the initial stateChangeDelegate of FirebaseAuth get called. Otherwise, we will interfere with that.
try await Task.sleep(for: .milliseconds(500))

do {
let details = SignupDetails.Builder()
.set(\.userId, value: email)
.set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford"))
.set(\.password, value: password)
.build()
try await service.signUp(signupDetails: details)
} catch {
if "\(error)".contains("accountAlreadyInUse") {
try await service.login(userId: email, password: password)
} else {
throw error
}
}
} catch {
logger.error("Failed setting up test account : \(error)")
throw error
}
}
}
31 changes: 9 additions & 22 deletions ENGAGEHF/Bluetooth/Devices/WeightScaleDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,12 @@ class WeightScaleDevice: BluetoothDevice, Identifiable {
if !service.$weightMeasurement.isPresent {
return
}

// TODO: can we avoid static property access?
MeasurementManager.manager.deviceInformation = deviceInformation
MeasurementManager.manager.weightScaleParams = service.features
MeasurementManager.manager.deviceName = name

MeasurementManager.manager.loadMeasurement(measurement)

MeasurementManager.manager.handleMeasurement(measurement, from: self)
}
}


// TODO: move somewhere!
extension WeightMeasurement.Unit {
var massUnit: String {
switch self {
Expand All @@ -62,32 +56,25 @@ extension WeightMeasurement.Unit {


extension WeightScaleDevice {
static func createMockDevice() -> WeightScaleDevice {
static func createMockDevice(weight: UInt16 = 8400, resolution: WeightScaleFeature.WeightResolution = .resolution5g) -> WeightScaleDevice {
let device = WeightScaleDevice()

device.deviceInformation.$manufacturerName.inject("Mock Weight Scale")
device.deviceInformation.$modelNumber.inject("1")
device.deviceInformation.$hardwareRevision.inject("2")
device.deviceInformation.$firmwareRevision.inject("1.0")

// TODO: adjust features to real world values
// mocks the values as reported by the real device
let features = WeightScaleFeature(
weightResolution: .resolution10g,
heightResolution: .resolution1mm,
options: .timeStampSupported,
.bmiSupported,
.multipleUsersSupported
weightResolution: resolution,
heightResolution: .unspecified,
options: .timeStampSupported
)

// TODO: adjustabale weight measurements?
let measurement = WeightMeasurement(
weight: 8400,
unit: .si,
timeStamp: DateTime(hours: 0, minutes: 0, seconds: 0),
userId: 42,
additionalInfo: .init(bmi: 20, height: 180)
weight: weight,
unit: .si
)
// TODO: adjust, real-world feature sets with measurement!

device.service.$features.inject(features)
device.service.$weightMeasurement.inject(measurement)
Expand Down
48 changes: 25 additions & 23 deletions ENGAGEHF/Bluetooth/MeasurementManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,10 @@ class MeasurementManager: Module, EnvironmentAccessible {

@ObservationIgnored @StandardActor private var standard: ENGAGEHFStandard
private let logger = Logger(subsystem: "ENGAGEHF", category: "MeasurementManager")

// TODO: why are these internal settable?
var deviceInformation: DeviceInformationService?
var weightScaleParams: WeightScaleFeature?
var deviceName: String?

private var deviceInformation: DeviceInformationService?
private var weightScaleParams: WeightScaleFeature?
private var deviceName: String?

var newMeasurement: HKQuantitySample?

Expand All @@ -61,24 +60,31 @@ class MeasurementManager: Module, EnvironmentAccessible {
}


// Called to reset measurement manager after taking a measurement
/// Called to reset measurement manager after taking a measurement
func clear() {
self.newMeasurement = nil
self.deviceInformation = nil
self.weightScaleParams = nil
self.deviceName = nil
}

// Called by WeightScaleDevice on change of WeightMeasurement Characteristic
func loadMeasurement(_ measurement: WeightMeasurement) {

func handleMeasurement(_ measurement: WeightMeasurement, from device: WeightScaleDevice) {
deviceInformation = device.deviceInformation
weightScaleParams = device.service.features
deviceName = device.name

loadMeasurement(measurement)
}

fileprivate func loadMeasurement(_ measurement: WeightMeasurement) {
// Convert to HKQuantitySample after downloading from Firestore
self.newMeasurement = convertToHKSample(measurement)
logger.info("Measurement loaded into MeasurementManager: \(measurement.weight)")
}

// Called by UI Sheet View to save the newMeasurement to firestore
/// Called by UI Sheet View to save the newMeasurement to firestore
func saveMeasurement() async throws {
if ProcessInfo.processInfo.isPreviewSimulator { // TODO: why is this here?
if ProcessInfo.processInfo.isPreviewSimulator {
try await Task.sleep(for: .seconds(5))
return
}
Expand All @@ -89,8 +95,8 @@ class MeasurementManager: Module, EnvironmentAccessible {
}

logger.info("Saving the following measurement: \(measurement.quantity.description)")
await standard.add(sample: measurement)
try await standard.addMeasurement(sample: measurement)

logger.info("Save successful!")
self.clear()
}
Expand Down Expand Up @@ -168,21 +174,17 @@ class MeasurementManager: Module, EnvironmentAccessible {


extension MeasurementManager {
// Call in preview simulator wrappers
// Loads a mock measurement to display in preview
/// Call in preview simulator wrappers.
///
/// Loads a mock measurement to display in preview.
func loadMockMeasurement() {
let device = WeightScaleDevice.createMockDevice()

// TODO: can we call processMeasurement for minimal code and highest coverage!

self.deviceName = device.name
self.deviceInformation = device.deviceInformation
self.weightScaleParams = device.service.features

guard let measurement = device.service.weightMeasurement else {
return // TODO: are we logging anything?
logger.error("Mock measurement was never injected!")
return
}

self.loadMeasurement(measurement)
handleMeasurement(measurement, from: device)
}
}
2 changes: 1 addition & 1 deletion ENGAGEHF/ENGAGEHF.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
//

import Spezi
import SpeziViews
import SpeziFirebaseAccount
import SpeziViews
import SwiftUI


Expand Down
1 change: 1 addition & 0 deletions ENGAGEHF/ENGAGEHFDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class ENGAGEHFDelegate: SpeziAppDelegate {

OnboardingDataSource()
MeasurementManager()
InvitationCodeModule()
}
}

Expand Down
12 changes: 10 additions & 2 deletions ENGAGEHF/ENGAGEHFStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,20 @@ actor ENGAGEHFStandard: Standard, EnvironmentAccessible, HealthKitConstraint, On
}
}

func add(sample: HKSample) async { // kept for compatibility with the Standard Constraint
do {
try await self.addMeasurement(sample: sample)
} catch {
logger.error("Could not store HealthKit sample: \(error)")
}
}


func add(sample: HKSample) async {
func addMeasurement(sample: HKSample) async throws {
do {
try await healthKitDocument(id: sample.id).setData(from: sample.resource)
} catch {
logger.error("Could not store HealthKit sample: \(error)")
throw FirestoreError(error)
}
}

Expand Down
Loading

0 comments on commit dc6d9cb

Please sign in to comment.