Files
Ascently/ios/AscentlyTests/AscentlyTests.swift
Atridad Lahiji 23de8a6fc6
All checks were successful
Ascently - Sync Deploy / build-and-push (push) Successful in 2m31s
Ascently - Docs Deploy / build-and-push (push) Successful in 3m30s
[All Platforms] 2.1.0 - Sync Optimizations
2025-10-15 18:17:19 -06:00

525 lines
18 KiB
Swift

import XCTest
final class AscentlyTests: XCTestCase {
override func setUpWithError() throws {
}
override func tearDownWithError() throws {
}
// MARK: - Data Validation Tests
func testDifficultyGradeComparison() throws {
// Test basic difficulty grade string comparison
let grade1 = "V5"
let grade2 = "V3"
let grade3 = "V5"
XCTAssertEqual(grade1, grade3)
XCTAssertNotEqual(grade1, grade2)
XCTAssertFalse(grade1.isEmpty)
}
func testClimbTypeValidation() throws {
// Test climb type validation
let validClimbTypes = ["ROPE", "BOULDER"]
for climbType in validClimbTypes {
XCTAssertTrue(validClimbTypes.contains(climbType))
XCTAssertFalse(climbType.isEmpty)
}
let invalidTypes = ["", "unknown", "invalid", "sport", "trad", "toprope"]
for invalidType in invalidTypes {
if !invalidType.isEmpty {
XCTAssertFalse(validClimbTypes.contains(invalidType))
}
}
}
func testDateFormatting() throws {
// Test ISO 8601 date formatting
let formatter = ISO8601DateFormatter()
let date = Date()
let formattedDate = formatter.string(from: date)
XCTAssertFalse(formattedDate.isEmpty)
XCTAssertTrue(formattedDate.contains("T"))
XCTAssertTrue(formattedDate.hasSuffix("Z"))
// Test parsing back
let parsedDate = formatter.date(from: formattedDate)
XCTAssertNotNil(parsedDate)
}
func testSessionDurationCalculation() throws {
// Test session duration calculation
let startTime = Date()
let endTime = Date(timeInterval: 3600, since: startTime) // 1 hour later
let duration = endTime.timeIntervalSince(startTime)
XCTAssertEqual(duration, 3600, accuracy: 1.0)
XCTAssertGreaterThan(duration, 0)
}
func testAttemptResultValidation() throws {
// Test attempt result validation
let validResults = ["completed", "failed", "flash", "project"]
for result in validResults {
XCTAssertTrue(validResults.contains(result))
XCTAssertFalse(result.isEmpty)
}
}
func testGymCreation() throws {
// Test gym model creation with basic validation
let gymName = "Test Climbing Gym"
let location = "Test City"
let supportedTypes = ["BOULDER", "ROPE"]
XCTAssertFalse(gymName.isEmpty)
XCTAssertFalse(location.isEmpty)
XCTAssertFalse(supportedTypes.isEmpty)
XCTAssertEqual(supportedTypes.count, 2)
XCTAssertTrue(supportedTypes.contains("BOULDER"))
XCTAssertTrue(supportedTypes.contains("ROPE"))
}
func testProblemValidation() throws {
// Test problem model validation
let problemName = "Test Problem"
let climbType = "BOULDER"
let difficulty = "V5"
let tags = ["overhang", "crimpy"]
XCTAssertFalse(problemName.isEmpty)
XCTAssertTrue(["BOULDER", "ROPE"].contains(climbType))
XCTAssertFalse(difficulty.isEmpty)
XCTAssertEqual(tags.count, 2)
XCTAssertTrue(tags.allSatisfy { !$0.isEmpty })
}
func testSessionStatusTransitions() throws {
// Test session status transitions
let validStatuses = ["planned", "active", "completed", "cancelled"]
for status in validStatuses {
XCTAssertTrue(validStatuses.contains(status))
XCTAssertFalse(status.isEmpty)
}
// Test status transitions logic
let initialStatus = "planned"
let activeStatus = "active"
let completedStatus = "completed"
XCTAssertNotEqual(initialStatus, activeStatus)
XCTAssertNotEqual(activeStatus, completedStatus)
}
func testUniqueIDGeneration() throws {
// Test unique ID generation using UUID
let id1 = UUID().uuidString
let id2 = UUID().uuidString
XCTAssertNotEqual(id1, id2)
XCTAssertFalse(id1.isEmpty)
XCTAssertFalse(id2.isEmpty)
XCTAssertEqual(id1.count, 36) // UUID string length
XCTAssertTrue(id1.contains("-"))
}
func testDataValidation() throws {
// Test basic data validation patterns
let emptyString = ""
let validString = "test"
let negativeNumber = -1
let positiveNumber = 5
let zeroNumber = 0
XCTAssertTrue(emptyString.isEmpty)
XCTAssertFalse(validString.isEmpty)
XCTAssertLessThan(negativeNumber, 0)
XCTAssertGreaterThan(positiveNumber, 0)
XCTAssertEqual(zeroNumber, 0)
}
// MARK: - Collection Tests
func testArrayOperations() throws {
// Test array operations for climb data
var problems: [String] = []
XCTAssertTrue(problems.isEmpty)
XCTAssertEqual(problems.count, 0)
problems.append("Problem 1")
problems.append("Problem 2")
XCTAssertFalse(problems.isEmpty)
XCTAssertEqual(problems.count, 2)
XCTAssertTrue(problems.contains("Problem 1"))
let filteredProblems = problems.filter { $0.contains("1") }
XCTAssertEqual(filteredProblems.count, 1)
}
func testDictionaryOperations() throws {
// Test dictionary operations for data storage
var gymData: [String: Any] = [:]
XCTAssertTrue(gymData.isEmpty)
gymData["name"] = "Test Gym"
gymData["location"] = "Test City"
gymData["types"] = ["BOULDER", "ROPE"]
XCTAssertFalse(gymData.isEmpty)
XCTAssertEqual(gymData.count, 3)
XCTAssertNotNil(gymData["name"])
if let name = gymData["name"] as? String {
XCTAssertEqual(name, "Test Gym")
} else {
XCTFail("Failed to cast gym name to String")
}
}
// MARK: - String and Numeric Tests
func testStringManipulation() throws {
// Test string operations common in climb data
let problemName = " Test Problem V5 "
let trimmedName = problemName.trimmingCharacters(in: .whitespacesAndNewlines)
let uppercaseName = trimmedName.uppercased()
let lowercaseName = trimmedName.lowercased()
XCTAssertEqual(trimmedName, "Test Problem V5")
XCTAssertEqual(uppercaseName, "TEST PROBLEM V5")
XCTAssertEqual(lowercaseName, "test problem v5")
let components = trimmedName.components(separatedBy: " ")
XCTAssertEqual(components.count, 3)
XCTAssertEqual(components.last, "V5")
}
func testNumericOperations() throws {
// Test numeric operations for climb ratings and statistics
let grades = [3, 5, 7, 4, 6]
let sum = grades.reduce(0, +)
let average = Double(sum) / Double(grades.count)
let maxGrade = grades.max() ?? 0
let minGrade = grades.min() ?? 0
XCTAssertEqual(sum, 25)
XCTAssertEqual(average, 5.0, accuracy: 0.01)
XCTAssertEqual(maxGrade, 7)
XCTAssertEqual(minGrade, 3)
}
// MARK: - JSON and Data Format Tests
func testJSONSerialization() throws {
// Test JSON serialization for basic data structures
let testData: [String: Any] = [
"id": "test123",
"name": "Test Gym",
"active": true,
"rating": 4.5,
"types": ["BOULDER", "ROPE"],
]
XCTAssertNoThrow({
let jsonData = try JSONSerialization.data(withJSONObject: testData)
XCTAssertFalse(jsonData.isEmpty)
let deserializedData =
try JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
XCTAssertNotNil(deserializedData)
XCTAssertEqual(deserializedData?["name"] as? String, "Test Gym")
})
}
func testDateSerialization() throws {
// Test date serialization for API compatibility
let date = Date()
let formatter = ISO8601DateFormatter()
let dateString = formatter.string(from: date)
let parsedDate = formatter.date(from: dateString)
XCTAssertNotNil(parsedDate)
XCTAssertEqual(date.timeIntervalSince1970, parsedDate!.timeIntervalSince1970, accuracy: 1.0)
}
// MARK: - Active Session Preservation Tests
func testActiveSessionPreservationDuringImport() throws {
let activeSessionId = UUID()
let gymId = UUID()
// Test data structure representing local active session
let localActiveSession: [String: Any] = [
"id": activeSessionId.uuidString,
"gymId": gymId.uuidString,
"status": "active",
"date": "2024-01-01",
"startTime": "2024-01-01T10:00:00Z",
]
// Test data structure representing server sessions (without the active one)
let serverSessions: [[String: Any]] = [
[
"id": UUID().uuidString,
"gymId": gymId.uuidString,
"status": "completed",
"date": "2023-12-31",
"startTime": "2023-12-31T15:00:00Z",
"endTime": "2023-12-31T17:00:00Z",
]
]
// Verify test setup
XCTAssertEqual(localActiveSession["status"] as? String, "active")
XCTAssertEqual(serverSessions.count, 1)
XCTAssertEqual(serverSessions[0]["status"] as? String, "completed")
// Verify that the active session ID is not in the server sessions
let serverSessionIds = serverSessions.compactMap { $0["id"] as? String }
XCTAssertFalse(serverSessionIds.contains(activeSessionId.uuidString))
// Test that we can identify an active session
if let status = localActiveSession["status"] as? String {
XCTAssertTrue(status == "active")
} else {
XCTFail("Failed to extract session status")
}
// Test session ID validation
if let sessionIdString = localActiveSession["id"] as? String,
let sessionId = UUID(uuidString: sessionIdString)
{
XCTAssertEqual(sessionId, activeSessionId)
} else {
XCTFail("Failed to parse session ID")
}
// Test that combining sessions preserves both local active and server completed
var combinedSessions = serverSessions
combinedSessions.append(localActiveSession)
XCTAssertEqual(combinedSessions.count, 2)
// Verify both session types are present
let hasActiveSession = combinedSessions.contains { session in
(session["status"] as? String) == "active"
}
let hasCompletedSession = combinedSessions.contains { session in
(session["status"] as? String) == "completed"
}
XCTAssertTrue(hasActiveSession, "Combined sessions should contain active 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")
}
}