- Create unit tests: DataLayerTests (27 tests for DATA-001–007), DataManagerExtendedTests (20 tests for TASK-005, TASK-012, TCOMP-003, THEME-001, QA-002), plus ValidationHelpers, TaskMetrics, StringExtensions, DoubleExtensions, DateUtils, DocumentHelpers, ErrorMessageParser - Create UI tests: AuthenticationTests, PasswordResetTests, OnboardingTests, TaskIntegration, ContractorIntegration, ResidenceIntegration, DocumentIntegration, DataLayer, Stability - Add UI test framework: AuthenticatedTestCase, ScreenObjects, TestFlows, TestAccountManager, TestAccountAPIClient, TestDataCleaner, TestDataSeeder - Add accessibility identifiers to password reset views for UI test support - Add greenfield test plan CSVs and update automated column for 27 test IDs - All 297 unit tests pass across 60 suites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
573 lines
22 KiB
Swift
573 lines
22 KiB
Swift
//
|
|
// DataLayerTests.swift
|
|
// CaseraTests
|
|
//
|
|
// Unit tests for the DATA layer domain (DATA-001 through DATA-007).
|
|
// Exercises Kotlin DataManager directly from Swift without launching the app.
|
|
//
|
|
// IMPORTANT: All suites that mutate DataManager (a shared singleton) are nested
|
|
// inside a .serialized parent suite to prevent concurrent test interference.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import Casera
|
|
import ComposeApp
|
|
|
|
// MARK: - Serialized Parent Suite (prevents concurrent DataManager mutations)
|
|
|
|
@Suite(.serialized)
|
|
struct DataLayerTests {
|
|
|
|
// MARK: - DATA-004: Cache Validation Tests
|
|
|
|
@Suite struct CacheValidationTests {
|
|
|
|
@Test func cacheTimeZeroIsInvalid() {
|
|
#expect(DataManager.shared.isCacheValid(cacheTime: 0) == false)
|
|
}
|
|
|
|
@Test func recentCacheTimeIsValid() {
|
|
// 5 minutes ago should be valid (well within the 1-hour timeout)
|
|
let fiveMinutesAgo = Int64(Date().timeIntervalSince1970 * 1000) - (5 * 60 * 1000)
|
|
#expect(DataManager.shared.isCacheValid(cacheTime: fiveMinutesAgo) == true)
|
|
}
|
|
|
|
@Test func expiredCacheTimeIsInvalid() {
|
|
// 2 hours ago should be invalid (past the 1-hour timeout)
|
|
let twoHoursAgo = Int64(Date().timeIntervalSince1970 * 1000) - (2 * 60 * 60 * 1000)
|
|
#expect(DataManager.shared.isCacheValid(cacheTime: twoHoursAgo) == false)
|
|
}
|
|
|
|
@Test func cacheTimeoutConstantIsOneHour() {
|
|
#expect(DataManager.shared.CACHE_TIMEOUT_MS == 3_600_000)
|
|
}
|
|
|
|
@Test func allCacheTimestampsStartAtZeroAfterClear() {
|
|
DataManager.shared.clear()
|
|
#expect(DataManager.shared.residencesCacheTime == 0)
|
|
#expect(DataManager.shared.myResidencesCacheTime == 0)
|
|
#expect(DataManager.shared.tasksCacheTime == 0)
|
|
#expect(DataManager.shared.contractorsCacheTime == 0)
|
|
#expect(DataManager.shared.documentsCacheTime == 0)
|
|
#expect(DataManager.shared.summaryCacheTime == 0)
|
|
}
|
|
}
|
|
|
|
// MARK: - DATA-005: Clear & ClearUserData Tests
|
|
|
|
@Suite struct ClearTests {
|
|
|
|
@Test func clearResetsAllCacheTimestamps() {
|
|
let cat = TaskCategory(id: 1, name: "Test", description: "", icon: "", color: "", displayOrder: 0)
|
|
DataManager.shared.setTaskCategories(categories: [cat])
|
|
|
|
DataManager.shared.clear()
|
|
|
|
#expect(DataManager.shared.residencesCacheTime == 0)
|
|
#expect(DataManager.shared.myResidencesCacheTime == 0)
|
|
#expect(DataManager.shared.tasksCacheTime == 0)
|
|
#expect(DataManager.shared.contractorsCacheTime == 0)
|
|
#expect(DataManager.shared.documentsCacheTime == 0)
|
|
#expect(DataManager.shared.summaryCacheTime == 0)
|
|
}
|
|
|
|
@Test func clearEmptiesLookupLists() {
|
|
// Populate all 5 lookup types
|
|
DataManager.shared.setTaskCategories(categories: [
|
|
TaskCategory(id: 1, name: "Cat", description: "", icon: "", color: "", displayOrder: 0)
|
|
])
|
|
DataManager.shared.setTaskPriorities(priorities: [
|
|
TaskPriority(id: 1, name: "High", level: 1, color: "red", displayOrder: 0)
|
|
])
|
|
DataManager.shared.setTaskFrequencies(frequencies: [
|
|
TaskFrequency(id: 1, name: "Weekly", days: nil, displayOrder: 0)
|
|
])
|
|
DataManager.shared.setResidenceTypes(types: [
|
|
ResidenceType(id: 1, name: "House")
|
|
])
|
|
DataManager.shared.setContractorSpecialties(specialties: [
|
|
ContractorSpecialty(id: 1, name: "Plumbing", description: nil, icon: nil, displayOrder: 0)
|
|
])
|
|
|
|
DataManager.shared.clear()
|
|
|
|
let categories = DataManager.shared.taskCategories.value as! [TaskCategory]
|
|
let priorities = DataManager.shared.taskPriorities.value as! [TaskPriority]
|
|
let frequencies = DataManager.shared.taskFrequencies.value as! [TaskFrequency]
|
|
let residenceTypes = DataManager.shared.residenceTypes.value as! [ResidenceType]
|
|
let specialties = DataManager.shared.contractorSpecialties.value as! [ContractorSpecialty]
|
|
|
|
#expect(categories.isEmpty)
|
|
#expect(priorities.isEmpty)
|
|
#expect(frequencies.isEmpty)
|
|
#expect(residenceTypes.isEmpty)
|
|
#expect(specialties.isEmpty)
|
|
}
|
|
|
|
@Test func clearResetsLookupsInitializedFlag() {
|
|
DataManager.shared.markLookupsInitialized()
|
|
let before = DataManager.shared.lookupsInitialized.value as! Bool
|
|
#expect(before == true)
|
|
|
|
DataManager.shared.clear()
|
|
|
|
let after = DataManager.shared.lookupsInitialized.value as! Bool
|
|
#expect(after == false)
|
|
}
|
|
|
|
@Test func clearUserDataKeepsLookups() {
|
|
DataManager.shared.clear()
|
|
|
|
// Populate lookups
|
|
DataManager.shared.setTaskCategories(categories: [
|
|
TaskCategory(id: 1, name: "Plumbing", description: "", icon: "", color: "", displayOrder: 0)
|
|
])
|
|
DataManager.shared.markLookupsInitialized()
|
|
|
|
// Clear user data only
|
|
DataManager.shared.clearUserData()
|
|
|
|
// Lookups should remain
|
|
let categories = DataManager.shared.taskCategories.value as! [TaskCategory]
|
|
#expect(categories.count == 1)
|
|
#expect(categories.first?.name == "Plumbing")
|
|
|
|
let initialized = DataManager.shared.lookupsInitialized.value as! Bool
|
|
#expect(initialized == true)
|
|
|
|
// Clean up
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func clearUserDataResetsCacheTimestamps() {
|
|
DataManager.shared.clear()
|
|
|
|
DataManager.shared.setTaskCategories(categories: [
|
|
TaskCategory(id: 1, name: "Test", description: "", icon: "", color: "", displayOrder: 0)
|
|
])
|
|
|
|
DataManager.shared.clearUserData()
|
|
|
|
#expect(DataManager.shared.residencesCacheTime == 0)
|
|
#expect(DataManager.shared.myResidencesCacheTime == 0)
|
|
#expect(DataManager.shared.tasksCacheTime == 0)
|
|
#expect(DataManager.shared.contractorsCacheTime == 0)
|
|
#expect(DataManager.shared.documentsCacheTime == 0)
|
|
#expect(DataManager.shared.summaryCacheTime == 0)
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
}
|
|
|
|
// MARK: - DATA-001, DATA-007: Lookup Setter & O(1) Getter Tests
|
|
|
|
@Suite struct LookupSetterTests {
|
|
|
|
@Test func setTaskCategoriesPopulatesList() {
|
|
DataManager.shared.clear()
|
|
|
|
let categories = [
|
|
TaskCategory(id: 1, name: "Plumbing", description: "Water", icon: "wrench", color: "blue", displayOrder: 1),
|
|
TaskCategory(id: 2, name: "HVAC", description: "Air", icon: "fan", color: "green", displayOrder: 2)
|
|
]
|
|
DataManager.shared.setTaskCategories(categories: categories)
|
|
|
|
let stored = DataManager.shared.taskCategories.value as! [TaskCategory]
|
|
#expect(stored.count == 2)
|
|
#expect(stored[0].name == "Plumbing")
|
|
#expect(stored[1].name == "HVAC")
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func setTaskCategoriesBuildsMappedLookup() {
|
|
DataManager.shared.clear()
|
|
|
|
let categories = [
|
|
TaskCategory(id: 10, name: "Electrical", description: "", icon: "", color: "", displayOrder: 0),
|
|
TaskCategory(id: 20, name: "Landscaping", description: "", icon: "", color: "", displayOrder: 1)
|
|
]
|
|
DataManager.shared.setTaskCategories(categories: categories)
|
|
|
|
// Verify the O(1) map-based getter works
|
|
let result10 = DataManager.shared.getTaskCategory(id: 10)
|
|
let result20 = DataManager.shared.getTaskCategory(id: 20)
|
|
#expect(result10?.name == "Electrical")
|
|
#expect(result20?.name == "Landscaping")
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func setTaskPrioritiesPopulatesListAndMap() {
|
|
DataManager.shared.clear()
|
|
|
|
let priorities = [
|
|
TaskPriority(id: 1, name: "High", level: 3, color: "red", displayOrder: 0),
|
|
TaskPriority(id: 2, name: "Medium", level: 2, color: "yellow", displayOrder: 1),
|
|
TaskPriority(id: 3, name: "Low", level: 1, color: "green", displayOrder: 2)
|
|
]
|
|
DataManager.shared.setTaskPriorities(priorities: priorities)
|
|
|
|
let stored = DataManager.shared.taskPriorities.value as! [TaskPriority]
|
|
#expect(stored.count == 3)
|
|
|
|
let high = DataManager.shared.getTaskPriority(id: 1)
|
|
#expect(high?.name == "High")
|
|
#expect(high?.level == 3)
|
|
|
|
let low = DataManager.shared.getTaskPriority(id: 3)
|
|
#expect(low?.name == "Low")
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func setTaskFrequenciesPopulatesListAndMap() {
|
|
DataManager.shared.clear()
|
|
|
|
let frequencies = [
|
|
TaskFrequency(id: 1, name: "Daily", days: 1, displayOrder: 0),
|
|
TaskFrequency(id: 2, name: "Weekly", days: 7, displayOrder: 1)
|
|
]
|
|
DataManager.shared.setTaskFrequencies(frequencies: frequencies)
|
|
|
|
let stored = DataManager.shared.taskFrequencies.value as! [TaskFrequency]
|
|
#expect(stored.count == 2)
|
|
|
|
let daily = DataManager.shared.getTaskFrequency(id: 1)
|
|
#expect(daily?.name == "Daily")
|
|
|
|
let weekly = DataManager.shared.getTaskFrequency(id: 2)
|
|
#expect(weekly?.name == "Weekly")
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func setResidenceTypesPopulatesListAndMap() {
|
|
DataManager.shared.clear()
|
|
|
|
let types = [
|
|
ResidenceType(id: 1, name: "Single Family"),
|
|
ResidenceType(id: 2, name: "Condo"),
|
|
ResidenceType(id: 3, name: "Townhouse")
|
|
]
|
|
DataManager.shared.setResidenceTypes(types: types)
|
|
|
|
let stored = DataManager.shared.residenceTypes.value as! [ResidenceType]
|
|
#expect(stored.count == 3)
|
|
|
|
let condo = DataManager.shared.getResidenceType(id: 2)
|
|
#expect(condo?.name == "Condo")
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func setContractorSpecialtiesPopulatesListAndMap() {
|
|
DataManager.shared.clear()
|
|
|
|
let specialties = [
|
|
ContractorSpecialty(id: 1, name: "Plumbing", description: "Pipes", icon: "wrench", displayOrder: 0),
|
|
ContractorSpecialty(id: 2, name: "Electrical", description: "Wiring", icon: "bolt", displayOrder: 1)
|
|
]
|
|
DataManager.shared.setContractorSpecialties(specialties: specialties)
|
|
|
|
let stored = DataManager.shared.contractorSpecialties.value as! [ContractorSpecialty]
|
|
#expect(stored.count == 2)
|
|
|
|
let plumbing = DataManager.shared.getContractorSpecialty(id: 1)
|
|
#expect(plumbing?.name == "Plumbing")
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func markLookupsInitializedSetsFlag() {
|
|
DataManager.shared.clear()
|
|
|
|
let before = DataManager.shared.lookupsInitialized.value as! Bool
|
|
#expect(before == false)
|
|
|
|
DataManager.shared.markLookupsInitialized()
|
|
|
|
let after = DataManager.shared.lookupsInitialized.value as! Bool
|
|
#expect(after == true)
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func getterReturnsNilForMissingId() {
|
|
DataManager.shared.clear()
|
|
|
|
DataManager.shared.setTaskCategories(categories: [
|
|
TaskCategory(id: 1, name: "Plumbing", description: "", icon: "", color: "", displayOrder: 0)
|
|
])
|
|
|
|
let result = DataManager.shared.getTaskCategory(id: 9999)
|
|
#expect(result == nil)
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
|
|
@Test func getterReturnsNilForNilId() {
|
|
DataManager.shared.clear()
|
|
|
|
DataManager.shared.setTaskCategories(categories: [
|
|
TaskCategory(id: 1, name: "Plumbing", description: "", icon: "", color: "", displayOrder: 0)
|
|
])
|
|
|
|
let result = DataManager.shared.getTaskCategory(id: nil)
|
|
#expect(result == nil)
|
|
|
|
DataManager.shared.clear()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - DATA-002: ETag Tests (requires local backend)
|
|
|
|
struct DataLayerETagTests {
|
|
|
|
/// Base URL for direct HTTP calls to the static_data endpoint.
|
|
/// Uses localhost for iOS simulator (127.0.0.1).
|
|
private static let staticDataURL = "http://127.0.0.1:8000/api/static_data/"
|
|
|
|
/// Synchronous HTTP GET with optional headers. Returns (statusCode, headers, data).
|
|
private func syncRequest(
|
|
url: String,
|
|
headers: [String: String] = [:]
|
|
) -> (statusCode: Int, headers: [String: String], data: Data?) {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var statusCode = 0
|
|
var responseHeaders: [String: String] = [:]
|
|
var responseData: Data?
|
|
|
|
var request = URLRequest(url: URL(string: url)!)
|
|
request.timeoutInterval = 10
|
|
for (key, value) in headers {
|
|
request.setValue(value, forHTTPHeaderField: key)
|
|
}
|
|
|
|
let task = URLSession.shared.dataTask(with: request) { data, response, _ in
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
statusCode = httpResponse.statusCode
|
|
for (key, value) in httpResponse.allHeaderFields {
|
|
responseHeaders["\(key)"] = "\(value)"
|
|
}
|
|
}
|
|
responseData = data
|
|
semaphore.signal()
|
|
}
|
|
task.resume()
|
|
semaphore.wait()
|
|
return (statusCode, responseHeaders, responseData)
|
|
}
|
|
|
|
/// Check if the local backend is reachable before running network tests.
|
|
private func isBackendReachable() -> Bool {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var reachable = false
|
|
|
|
var request = URLRequest(url: URL(string: Self.staticDataURL)!)
|
|
request.timeoutInterval = 3
|
|
request.httpMethod = "HEAD"
|
|
|
|
let task = URLSession.shared.dataTask(with: request) { _, response, _ in
|
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode > 0 {
|
|
reachable = true
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
task.resume()
|
|
semaphore.wait()
|
|
return reachable
|
|
}
|
|
|
|
@Test func staticDataEndpointReturnsETag() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping ETag test")
|
|
|
|
let (statusCode, headers, _) = syncRequest(url: Self.staticDataURL)
|
|
#expect(statusCode == 200)
|
|
|
|
// ETag header should be present (case-insensitive check)
|
|
let etag = headers["Etag"] ?? headers["ETag"] ?? headers["etag"]
|
|
#expect(etag != nil, "Response should include an ETag header")
|
|
#expect(etag?.isEmpty == false)
|
|
}
|
|
|
|
@Test func conditionalRequestReturns304WhenDataUnchanged() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping ETag test")
|
|
|
|
// First request: get the ETag
|
|
let (statusCode1, headers1, _) = syncRequest(url: Self.staticDataURL)
|
|
#expect(statusCode1 == 200)
|
|
|
|
let etag = headers1["Etag"] ?? headers1["ETag"] ?? headers1["etag"]
|
|
try #require(etag != nil, "First response must include ETag")
|
|
|
|
// Second request: send If-None-Match with valid ETag
|
|
let (statusCode2, headers2, _) = syncRequest(
|
|
url: Self.staticDataURL,
|
|
headers: ["If-None-Match": etag!]
|
|
)
|
|
|
|
// Server should return 304 (data unchanged) or 200 with a new ETag
|
|
// (if data was modified between requests). Both are valid ETag behavior.
|
|
let validResponses: Set<Int> = [200, 304]
|
|
#expect(validResponses.contains(statusCode2),
|
|
"Conditional request should return 304 (unchanged) or 200 (new data), got \(statusCode2)")
|
|
|
|
if statusCode2 == 200 {
|
|
// If server returned 200, it should include a new ETag
|
|
let newEtag = headers2["Etag"] ?? headers2["ETag"] ?? headers2["etag"]
|
|
#expect(newEtag != nil, "200 response should include a new ETag")
|
|
}
|
|
}
|
|
|
|
@Test func staleETagReturns200() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping ETag test")
|
|
|
|
// Send a bogus ETag — server should return 200 with fresh data
|
|
let (statusCode, _, data) = syncRequest(
|
|
url: Self.staticDataURL,
|
|
headers: ["If-None-Match": "\"bogus-etag-value\""]
|
|
)
|
|
#expect(statusCode == 200, "Stale/bogus ETag should result in 200 with fresh data")
|
|
#expect(data != nil && (data?.count ?? 0) > 0)
|
|
}
|
|
}
|
|
|
|
// MARK: - DATA-003, DATA-007: API Schema Validation Tests (requires local backend)
|
|
|
|
struct DataLayerAPISchemaTests {
|
|
|
|
private static let staticDataURL = "http://127.0.0.1:8000/api/static_data/"
|
|
|
|
/// Synchronous HTTP GET returning decoded JSON dictionary.
|
|
private func fetchStaticData() -> [String: Any]? {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var result: [String: Any]?
|
|
|
|
var request = URLRequest(url: URL(string: Self.staticDataURL)!)
|
|
request.timeoutInterval = 10
|
|
|
|
let task = URLSession.shared.dataTask(with: request) { data, response, _ in
|
|
if let httpResponse = response as? HTTPURLResponse,
|
|
httpResponse.statusCode == 200,
|
|
let data = data {
|
|
result = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
task.resume()
|
|
semaphore.wait()
|
|
return result
|
|
}
|
|
|
|
private func isBackendReachable() -> Bool {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
var reachable = false
|
|
|
|
var request = URLRequest(url: URL(string: Self.staticDataURL)!)
|
|
request.timeoutInterval = 3
|
|
request.httpMethod = "HEAD"
|
|
|
|
let task = URLSession.shared.dataTask(with: request) { _, response, _ in
|
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode > 0 {
|
|
reachable = true
|
|
}
|
|
semaphore.signal()
|
|
}
|
|
task.resume()
|
|
semaphore.wait()
|
|
return reachable
|
|
}
|
|
|
|
@Test func staticDataContainsAllRequiredLookupTypes() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")
|
|
|
|
let data = try #require(fetchStaticData(), "Failed to fetch static data")
|
|
|
|
let requiredKeys = [
|
|
"residence_types",
|
|
"task_frequencies",
|
|
"task_priorities",
|
|
"task_categories",
|
|
"contractor_specialties"
|
|
]
|
|
|
|
for key in requiredKeys {
|
|
#expect(data[key] != nil, "static_data response missing required key: \(key)")
|
|
let array = data[key] as? [[String: Any]]
|
|
#expect(array != nil, "\(key) should be an array of objects")
|
|
#expect((array?.count ?? 0) > 0, "\(key) should not be empty")
|
|
}
|
|
}
|
|
|
|
@Test func allLookupItemsHaveIdAndName() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")
|
|
|
|
let data = try #require(fetchStaticData(), "Failed to fetch static data")
|
|
|
|
let lookupKeys = [
|
|
"residence_types",
|
|
"task_frequencies",
|
|
"task_priorities",
|
|
"task_categories",
|
|
"contractor_specialties"
|
|
]
|
|
|
|
for key in lookupKeys {
|
|
guard let items = data[key] as? [[String: Any]] else { continue }
|
|
for (index, item) in items.enumerated() {
|
|
#expect(item["id"] != nil, "\(key)[\(index)] missing 'id'")
|
|
#expect(item["name"] != nil, "\(key)[\(index)] missing 'name'")
|
|
#expect(item["name"] as? String != "", "\(key)[\(index)] has empty 'name'")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test func allIdsAreUniquePerLookupType() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")
|
|
|
|
let data = try #require(fetchStaticData(), "Failed to fetch static data")
|
|
|
|
let lookupKeys = [
|
|
"residence_types",
|
|
"task_frequencies",
|
|
"task_priorities",
|
|
"task_categories",
|
|
"contractor_specialties"
|
|
]
|
|
|
|
for key in lookupKeys {
|
|
guard let items = data[key] as? [[String: Any]] else { continue }
|
|
let ids = items.compactMap { $0["id"] as? Int }
|
|
let uniqueIds = Set(ids)
|
|
#expect(ids.count == uniqueIds.count, "\(key) has duplicate IDs — would break associateBy map")
|
|
}
|
|
}
|
|
|
|
@Test func taskCategoriesHaveColorAndIcon() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")
|
|
|
|
let data = try #require(fetchStaticData(), "Failed to fetch static data")
|
|
let categories = try #require(data["task_categories"] as? [[String: Any]], "Missing task_categories")
|
|
|
|
for (index, cat) in categories.enumerated() {
|
|
#expect(cat["color"] != nil, "task_categories[\(index)] missing 'color'")
|
|
#expect(cat["icon"] != nil, "task_categories[\(index)] missing 'icon'")
|
|
}
|
|
}
|
|
|
|
@Test func taskPrioritiesHaveLevelAndColor() throws {
|
|
try #require(isBackendReachable(), "Local backend not reachable — skipping API schema test")
|
|
|
|
let data = try #require(fetchStaticData(), "Failed to fetch static data")
|
|
let priorities = try #require(data["task_priorities"] as? [[String: Any]], "Missing task_priorities")
|
|
|
|
for (index, pri) in priorities.enumerated() {
|
|
#expect(pri["level"] != nil, "task_priorities[\(index)] missing 'level'")
|
|
#expect(pri["color"] != nil, "task_priorities[\(index)] missing 'color'")
|
|
}
|
|
}
|
|
}
|