iOS Build 22
This commit is contained in:
@@ -465,7 +465,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -513,7 +513,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -602,7 +602,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -632,7 +632,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
|
|||||||
Binary file not shown.
@@ -96,6 +96,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
loadSessions()
|
loadSessions()
|
||||||
loadAttempts()
|
loadAttempts()
|
||||||
loadActiveSession()
|
loadActiveSession()
|
||||||
|
|
||||||
|
// Clean up orphaned data after loading
|
||||||
|
cleanupOrphanedData()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadGyms() {
|
private func loadGyms() {
|
||||||
@@ -286,7 +289,16 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deleteProblem(_ problem: Problem) {
|
func deleteProblem(_ problem: Problem) {
|
||||||
// Delete associated attempts first
|
// Track deletion of the problem
|
||||||
|
trackDeletion(itemId: problem.id.uuidString, itemType: "problem")
|
||||||
|
|
||||||
|
// Find and track all attempts for this problem as deleted
|
||||||
|
let problemAttempts = attempts.filter { $0.problemId == problem.id }
|
||||||
|
for attempt in problemAttempts {
|
||||||
|
trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete associated attempts
|
||||||
attempts.removeAll { $0.problemId == problem.id }
|
attempts.removeAll { $0.problemId == problem.id }
|
||||||
saveAttempts()
|
saveAttempts()
|
||||||
|
|
||||||
@@ -295,7 +307,6 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
|
|
||||||
// Delete the problem
|
// Delete the problem
|
||||||
problems.removeAll { $0.id == problem.id }
|
problems.removeAll { $0.id == problem.id }
|
||||||
trackDeletion(itemId: problem.id.uuidString, itemType: "problem")
|
|
||||||
saveProblems()
|
saveProblems()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
|
|
||||||
@@ -410,7 +421,16 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deleteSession(_ session: ClimbSession) {
|
func deleteSession(_ session: ClimbSession) {
|
||||||
// Delete associated attempts first
|
// Track deletion of the session
|
||||||
|
trackDeletion(itemId: session.id.uuidString, itemType: "session")
|
||||||
|
|
||||||
|
// Find and track all attempts for this session as deleted
|
||||||
|
let sessionAttempts = attempts.filter { $0.sessionId == session.id }
|
||||||
|
for attempt in sessionAttempts {
|
||||||
|
trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete associated attempts
|
||||||
attempts.removeAll { $0.sessionId == session.id }
|
attempts.removeAll { $0.sessionId == session.id }
|
||||||
saveAttempts()
|
saveAttempts()
|
||||||
|
|
||||||
@@ -422,7 +442,6 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
|
|
||||||
// Delete the session
|
// Delete the session
|
||||||
sessions.removeAll { $0.id == session.id }
|
sessions.removeAll { $0.id == session.id }
|
||||||
trackDeletion(itemId: session.id.uuidString, itemType: "session")
|
|
||||||
saveSessions()
|
saveSessions()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
|
|
||||||
@@ -548,6 +567,162 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
return gym(withId: mostUsedGymId)
|
return gym(withId: mostUsedGymId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clean up orphaned data - removes attempts that reference non-existent sessions
|
||||||
|
/// and removes duplicate attempts. This ensures data integrity and prevents
|
||||||
|
/// orphaned attempts from appearing in widgets
|
||||||
|
private func cleanupOrphanedData() {
|
||||||
|
let validSessionIds = Set(sessions.map { $0.id })
|
||||||
|
let validProblemIds = Set(problems.map { $0.id })
|
||||||
|
let validGymIds = Set(gyms.map { $0.id })
|
||||||
|
|
||||||
|
let initialAttemptCount = attempts.count
|
||||||
|
|
||||||
|
// Remove attempts that reference deleted sessions or problems
|
||||||
|
let orphanedAttempts = attempts.filter { attempt in
|
||||||
|
!validSessionIds.contains(attempt.sessionId)
|
||||||
|
|| !validProblemIds.contains(attempt.problemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !orphanedAttempts.isEmpty {
|
||||||
|
print("🧹 Cleaning up \(orphanedAttempts.count) orphaned attempts")
|
||||||
|
|
||||||
|
// Track these as deleted to prevent sync from re-introducing them
|
||||||
|
for attempt in orphanedAttempts {
|
||||||
|
trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove orphaned attempts
|
||||||
|
attempts.removeAll { attempt in
|
||||||
|
!validSessionIds.contains(attempt.sessionId)
|
||||||
|
|| !validProblemIds.contains(attempt.problemId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicate attempts (same session, problem, and timestamp within 1 second)
|
||||||
|
var seenAttempts: Set<String> = []
|
||||||
|
var duplicateIds: [UUID] = []
|
||||||
|
|
||||||
|
for attempt in attempts.sorted(by: { $0.timestamp < $1.timestamp }) {
|
||||||
|
// Create a unique key based on session, problem, and rounded timestamp
|
||||||
|
let timestampKey = Int(attempt.timestamp.timeIntervalSince1970)
|
||||||
|
let key =
|
||||||
|
"\(attempt.sessionId.uuidString)_\(attempt.problemId.uuidString)_\(timestampKey)"
|
||||||
|
|
||||||
|
if seenAttempts.contains(key) {
|
||||||
|
duplicateIds.append(attempt.id)
|
||||||
|
print("🧹 Found duplicate attempt: \(attempt.id)")
|
||||||
|
} else {
|
||||||
|
seenAttempts.insert(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !duplicateIds.isEmpty {
|
||||||
|
print("🧹 Removing \(duplicateIds.count) duplicate attempts")
|
||||||
|
|
||||||
|
// Track duplicates as deleted
|
||||||
|
for attemptId in duplicateIds {
|
||||||
|
trackDeletion(itemId: attemptId.uuidString, itemType: "attempt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
attempts.removeAll { duplicateIds.contains($0.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if initialAttemptCount != attempts.count {
|
||||||
|
saveAttempts()
|
||||||
|
let removedCount = initialAttemptCount - attempts.count
|
||||||
|
print(
|
||||||
|
"✅ Cleanup complete. Removed \(removedCount) attempts. Remaining: \(attempts.count)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove problems that reference deleted gyms
|
||||||
|
let orphanedProblems = problems.filter { problem in
|
||||||
|
!validGymIds.contains(problem.gymId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !orphanedProblems.isEmpty {
|
||||||
|
print("🧹 Cleaning up \(orphanedProblems.count) orphaned problems")
|
||||||
|
|
||||||
|
for problem in orphanedProblems {
|
||||||
|
trackDeletion(itemId: problem.id.uuidString, itemType: "problem")
|
||||||
|
}
|
||||||
|
|
||||||
|
problems.removeAll { problem in
|
||||||
|
!validGymIds.contains(problem.gymId)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProblems()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sessions that reference deleted gyms
|
||||||
|
let orphanedSessions = sessions.filter { session in
|
||||||
|
!validGymIds.contains(session.gymId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !orphanedSessions.isEmpty {
|
||||||
|
print("🧹 Cleaning up \(orphanedSessions.count) orphaned sessions")
|
||||||
|
|
||||||
|
for session in orphanedSessions {
|
||||||
|
trackDeletion(itemId: session.id.uuidString, itemType: "session")
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions.removeAll { session in
|
||||||
|
!validGymIds.contains(session.gymId)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSessions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate data integrity and return a report
|
||||||
|
/// This can be called manually to check for issues
|
||||||
|
func validateDataIntegrity() -> String {
|
||||||
|
let validSessionIds = Set(sessions.map { $0.id })
|
||||||
|
let validProblemIds = Set(problems.map { $0.id })
|
||||||
|
let validGymIds = Set(gyms.map { $0.id })
|
||||||
|
|
||||||
|
let orphanedAttempts = attempts.filter { attempt in
|
||||||
|
!validSessionIds.contains(attempt.sessionId)
|
||||||
|
|| !validProblemIds.contains(attempt.problemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let orphanedProblems = problems.filter { problem in
|
||||||
|
!validGymIds.contains(problem.gymId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let orphanedSessions = sessions.filter { session in
|
||||||
|
!validGymIds.contains(session.gymId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var report = "Data Integrity Report:\n"
|
||||||
|
report += "---------------------\n"
|
||||||
|
report += "Gyms: \(gyms.count)\n"
|
||||||
|
report += "Problems: \(problems.count)\n"
|
||||||
|
report += "Sessions: \(sessions.count)\n"
|
||||||
|
report += "Attempts: \(attempts.count)\n"
|
||||||
|
report += "\nOrphaned Data:\n"
|
||||||
|
report += "Orphaned Attempts: \(orphanedAttempts.count)\n"
|
||||||
|
report += "Orphaned Problems: \(orphanedProblems.count)\n"
|
||||||
|
report += "Orphaned Sessions: \(orphanedSessions.count)\n"
|
||||||
|
|
||||||
|
if orphanedAttempts.isEmpty && orphanedProblems.isEmpty && orphanedSessions.isEmpty {
|
||||||
|
report += "\n✅ No integrity issues found"
|
||||||
|
} else {
|
||||||
|
report += "\n⚠️ Issues found - run cleanup to fix"
|
||||||
|
}
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manually trigger cleanup of orphaned data
|
||||||
|
/// This can be called from settings or debug menu
|
||||||
|
func manualDataCleanup() {
|
||||||
|
cleanupOrphanedData()
|
||||||
|
successMessage = "Data cleanup completed"
|
||||||
|
clearMessageAfterDelay()
|
||||||
|
}
|
||||||
|
|
||||||
func resetAllData(showSuccessMessage: Bool = true) {
|
func resetAllData(showSuccessMessage: Bool = true) {
|
||||||
gyms.removeAll()
|
gyms.removeAll()
|
||||||
problems.removeAll()
|
problems.removeAll()
|
||||||
|
|||||||
@@ -326,4 +326,203 @@ final class OpenClimbTests: XCTestCase {
|
|||||||
XCTAssertTrue(hasActiveSession, "Combined sessions should contain active session")
|
XCTAssertTrue(hasActiveSession, "Combined sessions should contain active session")
|
||||||
XCTAssertTrue(hasCompletedSession, "Combined sessions should contain completed session")
|
XCTAssertTrue(hasCompletedSession, "Combined sessions should contain completed session")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Orphaned Data Cleanup Tests
|
||||||
|
|
||||||
|
func testOrphanedAttemptDetection() throws {
|
||||||
|
// Test that we can detect orphaned attempts (attempts referencing non-existent sessions)
|
||||||
|
|
||||||
|
let validSessionId = UUID()
|
||||||
|
let deletedSessionId = UUID()
|
||||||
|
let validProblemId = UUID()
|
||||||
|
|
||||||
|
// Simulate a list of valid sessions
|
||||||
|
let validSessions = [validSessionId]
|
||||||
|
|
||||||
|
// Simulate attempts - one valid, one orphaned
|
||||||
|
let validAttempt: [String: Any] = [
|
||||||
|
"id": UUID().uuidString,
|
||||||
|
"sessionId": validSessionId.uuidString,
|
||||||
|
"problemId": validProblemId.uuidString,
|
||||||
|
"result": "completed",
|
||||||
|
]
|
||||||
|
|
||||||
|
let orphanedAttempt: [String: Any] = [
|
||||||
|
"id": UUID().uuidString,
|
||||||
|
"sessionId": deletedSessionId.uuidString,
|
||||||
|
"problemId": validProblemId.uuidString,
|
||||||
|
"result": "completed",
|
||||||
|
]
|
||||||
|
|
||||||
|
let allAttempts = [validAttempt, orphanedAttempt]
|
||||||
|
|
||||||
|
// Filter to find orphaned attempts
|
||||||
|
let orphaned = allAttempts.filter { attempt in
|
||||||
|
guard let sessionIdString = attempt["sessionId"] as? String,
|
||||||
|
let sessionId = UUID(uuidString: sessionIdString)
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !validSessions.contains(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(orphaned.count, 1, "Should detect exactly one orphaned attempt")
|
||||||
|
XCTAssertEqual(orphaned[0]["sessionId"] as? String, deletedSessionId.uuidString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOrphanedAttemptRemoval() throws {
|
||||||
|
// Test that orphaned attempts can be properly removed from a list
|
||||||
|
|
||||||
|
let validSessionId = UUID()
|
||||||
|
let deletedSessionId = UUID()
|
||||||
|
let problemId = UUID()
|
||||||
|
|
||||||
|
let validSessions = Set([validSessionId])
|
||||||
|
|
||||||
|
// Create test attempts
|
||||||
|
var attempts: [[String: Any]] = [
|
||||||
|
[
|
||||||
|
"id": UUID().uuidString,
|
||||||
|
"sessionId": validSessionId.uuidString,
|
||||||
|
"problemId": problemId.uuidString,
|
||||||
|
"result": "completed",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"id": UUID().uuidString,
|
||||||
|
"sessionId": deletedSessionId.uuidString,
|
||||||
|
"problemId": problemId.uuidString,
|
||||||
|
"result": "failed",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"id": UUID().uuidString,
|
||||||
|
"sessionId": validSessionId.uuidString,
|
||||||
|
"problemId": problemId.uuidString,
|
||||||
|
"result": "flash",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
XCTAssertEqual(attempts.count, 3, "Should start with 3 attempts")
|
||||||
|
|
||||||
|
// Remove orphaned attempts
|
||||||
|
attempts.removeAll { attempt in
|
||||||
|
guard let sessionIdString = attempt["sessionId"] as? String,
|
||||||
|
let sessionId = UUID(uuidString: sessionIdString)
|
||||||
|
else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !validSessions.contains(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(attempts.count, 2, "Should have 2 attempts after cleanup")
|
||||||
|
|
||||||
|
// Verify remaining attempts are all valid
|
||||||
|
for attempt in attempts {
|
||||||
|
if let sessionIdString = attempt["sessionId"] as? String,
|
||||||
|
let sessionId = UUID(uuidString: sessionIdString)
|
||||||
|
{
|
||||||
|
XCTAssertTrue(
|
||||||
|
validSessions.contains(sessionId),
|
||||||
|
"All remaining attempts should reference valid sessions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCascadeDeleteSessionWithAttempts() throws {
|
||||||
|
// Test that deleting a session properly tracks all its attempts as deleted
|
||||||
|
|
||||||
|
let sessionId = UUID()
|
||||||
|
let problemId = UUID()
|
||||||
|
|
||||||
|
// Create attempts for this session
|
||||||
|
let sessionAttempts: [[String: Any]] = [
|
||||||
|
[
|
||||||
|
"id": UUID().uuidString, "sessionId": sessionId.uuidString,
|
||||||
|
"problemId": problemId.uuidString,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"id": UUID().uuidString, "sessionId": sessionId.uuidString,
|
||||||
|
"problemId": problemId.uuidString,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"id": UUID().uuidString, "sessionId": sessionId.uuidString,
|
||||||
|
"problemId": problemId.uuidString,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
XCTAssertEqual(sessionAttempts.count, 3, "Session should have 3 attempts")
|
||||||
|
|
||||||
|
// Simulate tracking deletions
|
||||||
|
var deletedItems: [String] = []
|
||||||
|
|
||||||
|
// Add session to deleted items
|
||||||
|
deletedItems.append(sessionId.uuidString)
|
||||||
|
|
||||||
|
// Add all attempts to deleted items
|
||||||
|
for attempt in sessionAttempts {
|
||||||
|
if let attemptId = attempt["id"] as? String {
|
||||||
|
deletedItems.append(attemptId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(deletedItems.count, 4, "Should track 1 session + 3 attempts as deleted")
|
||||||
|
XCTAssertTrue(deletedItems.contains(sessionId.uuidString), "Should track session deletion")
|
||||||
|
|
||||||
|
// Verify all attempt IDs are tracked
|
||||||
|
let attemptIds = sessionAttempts.compactMap { $0["id"] as? String }
|
||||||
|
for attemptId in attemptIds {
|
||||||
|
XCTAssertTrue(
|
||||||
|
deletedItems.contains(attemptId), "Should track attempt \(attemptId) deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDataIntegrityValidation() throws {
|
||||||
|
// Test data integrity validation logic
|
||||||
|
|
||||||
|
let gymId = UUID()
|
||||||
|
let sessionId = UUID()
|
||||||
|
let problemId = UUID()
|
||||||
|
|
||||||
|
// Valid data setup
|
||||||
|
let gyms = [gymId]
|
||||||
|
let sessions = [(id: sessionId, gymId: gymId)]
|
||||||
|
let problems = [(id: problemId, gymId: gymId)]
|
||||||
|
let attempts = [
|
||||||
|
(id: UUID(), sessionId: sessionId, problemId: problemId),
|
||||||
|
(id: UUID(), sessionId: sessionId, problemId: problemId),
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validate that all relationships are correct
|
||||||
|
let validGyms = Set(gyms)
|
||||||
|
let validSessions = Set(sessions.map { $0.id })
|
||||||
|
let validProblems = Set(problems.map { $0.id })
|
||||||
|
|
||||||
|
// Check sessions reference valid gyms
|
||||||
|
for session in sessions {
|
||||||
|
XCTAssertTrue(validGyms.contains(session.gymId), "Session should reference valid gym")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check problems reference valid gyms
|
||||||
|
for problem in problems {
|
||||||
|
XCTAssertTrue(validGyms.contains(problem.gymId), "Problem should reference valid gym")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check attempts reference valid sessions and problems
|
||||||
|
for attempt in attempts {
|
||||||
|
XCTAssertTrue(
|
||||||
|
validSessions.contains(attempt.sessionId), "Attempt should reference valid session")
|
||||||
|
XCTAssertTrue(
|
||||||
|
validProblems.contains(attempt.problemId), "Attempt should reference valid problem")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test integrity check passes
|
||||||
|
let hasOrphanedSessions = sessions.contains { !validGyms.contains($0.gymId) }
|
||||||
|
let hasOrphanedProblems = problems.contains { !validGyms.contains($0.gymId) }
|
||||||
|
let hasOrphanedAttempts = attempts.contains {
|
||||||
|
!validSessions.contains($0.sessionId) || !validProblems.contains($0.problemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertFalse(hasOrphanedSessions, "Should not have orphaned sessions")
|
||||||
|
XCTAssertFalse(hasOrphanedProblems, "Should not have orphaned problems")
|
||||||
|
XCTAssertFalse(hasOrphanedAttempts, "Should not have orphaned attempts")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user